Merge feature/calibration-tab: калибровки уровней и частоты (паритет с Thetis)

Вкладка Calibration + всё, что нужно, чтобы шкала dBm и частота были
абсолютными на обоих бэкендах.

- Детектор мощности/КСВ Alex-моста по моделям ANAN, per-band множитель
  FWD/REV, калибровка напряжения и тока питания.
- Офсеты S-метра и спектра/водопада — раздельными таблицами на диапазон и на
  слот трансвертера, по активному железу: у openHPSDR HF-план, у Pluto
  VHF/UHF-план, секции JSON ключуются по MAC.
- Регулируемое усиление входа больше не двигает шкалу: шаговый аттенюатор
  openHPSDR (ставший плавным 0..31 дБ с ползунком в RX-блоке и памятью на
  диапазон/слот) и hardwaregain AD936x, который в режимах AGC железо крутит
  само.
- Частотная калибровка опорника (ppm): у HPSDR — частотное слово DDC/DUC, у
  Pluto — xo_correction. Ручные LO Error / TO Error трансвертеров остаются
  сверху; в слоте QO-100 сам транспондер-offset теперь стандартный, а уход
  тракта правится отдельной поправкой.

Проверено на железе (openHPSDR и Pluto).
This commit is contained in:
2026-07-21 12:38:48 +03:00
14 changed files with 1170 additions and 62 deletions
+24 -4
View File
@@ -156,6 +156,9 @@ type
FMainDither: Boolean; FMainDither: Boolean;
FMainRandom: Boolean; FMainRandom: Boolean;
// Частотная калибровка опорника радио (ppm) — вносится в частотное слово
// (тактовый генератор радио менять нечем, поэтому правим запрос).
FFreqCalPPM: Double;
// Текущее состояние для построения HP пакетов // Текущее состояние для построения HP пакетов
FCurrentRXFreq: Double; FCurrentRXFreq: Double;
FCurrentTXFreq: Double; FCurrentTXFreq: Double;
@@ -260,6 +263,9 @@ type
procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte; procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte;
Transmitting, PAEnabled, AlexEnabled: Boolean); override; Transmitting, PAEnabled, AlexEnabled: Boolean); override;
procedure SetStepAtten(dB: Byte); override; procedure SetStepAtten(dB: Byte); override;
procedure SetFreqCalPPM(P: Double); override;
// Частота с поправкой опорника — для частотного слова DDC/DUC.
function CalFreq(Hz: Double): Double;
procedure SetAlexConfig(const A: TAlexSettings); override; procedure SetAlexConfig(const A: TAlexSettings); override;
// Включение/настройка XVTR-режима. Все параметры применяются мгновенно // Включение/настройка XVTR-режима. Все параметры применяются мгновенно
// (отправляется HP-пакет). Когда AEnable=False, XVTR-bit гасится, // (отправляется HP-пакет). Когда AEnable=False, XVTR-bit гасится,
@@ -736,6 +742,7 @@ begin
FWBUpdateRateMS := 70; FWBUpdateRateMS := 70;
FWBFrameCount := 0; FWBFrameCount := 0;
FWBSeqValid := False; FWBSeqValid := False;
FFreqCalPPM := 0.0;
FCurrentRXFreq := 7100000; FCurrentRXFreq := 7100000;
FCurrentTXFreq := 7100000; FCurrentTXFreq := 7100000;
FCurrentDrive := 0; FCurrentDrive := 0;
@@ -1693,6 +1700,19 @@ begin
FAlexEnabled := AlexEnabled; FAlexEnabled := AlexEnabled;
end; 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); procedure THPSDRNetwork.SetStepAtten(dB: Byte);
begin begin
if dB > 31 then dB := 31; if dB > 31 then dB := 31;
@@ -1737,7 +1757,7 @@ begin
DDCBase := DDCBaseIndex; DDCBase := DDCBaseIndex;
// DDC RX frequency (bytes 9 + DDC*4) // DDC RX frequency (bytes 9 + DDC*4)
Ph := FreqToPhaseWord(FCurrentRXFreq); Ph := FreqToPhaseWord(CalFreq(FCurrentRXFreq));
Buf[9 + DDCBase*4] := (Ph shr 24) and $FF; Buf[9 + DDCBase*4] := (Ph shr 24) and $FF;
Buf[10 + DDCBase*4] := (Ph shr 16) and $FF; Buf[10 + DDCBase*4] := (Ph shr 16) and $FF;
Buf[11 + DDCBase*4] := (Ph shr 8) and $FF; Buf[11 + DDCBase*4] := (Ph shr 8) and $FF;
@@ -1748,7 +1768,7 @@ begin
for ddc := 0 to MAX_DDCS - 1 do for ddc := 0 to MAX_DDCS - 1 do
if FPanDDCEnabled[ddc] then if FPanDDCEnabled[ddc] then
begin begin
Ph := FreqToPhaseWord(FPanDDCFreqHz[ddc]); Ph := FreqToPhaseWord(CalFreq(FPanDDCFreqHz[ddc]));
Buf[9 + ddc*4] := (Ph shr 24) and $FF; Buf[9 + ddc*4] := (Ph shr 24) and $FF;
Buf[10 + ddc*4] := (Ph shr 16) and $FF; Buf[10 + ddc*4] := (Ph shr 16) and $FF;
Buf[11 + ddc*4] := (Ph shr 8) and $FF; Buf[11 + ddc*4] := (Ph shr 8) and $FF;
@@ -1759,7 +1779,7 @@ begin
// DDCBase=2 ⇒ с частотным словом главного DDC (bytes 17..20) не пересекаемся. // DDCBase=2 ⇒ с частотным словом главного DDC (bytes 17..20) не пересекаемся.
if FPSTXActive then if FPSTXActive then
begin begin
Ph := FreqToPhaseWord(FCurrentTXFreq); Ph := FreqToPhaseWord(CalFreq(FCurrentTXFreq));
Buf[9] := (Ph shr 24) and $FF; Buf[9] := (Ph shr 24) and $FF;
Buf[10] := (Ph shr 16) and $FF; Buf[10] := (Ph shr 16) and $FF;
Buf[11] := (Ph shr 8) and $FF; Buf[11] := (Ph shr 8) and $FF;
@@ -1771,7 +1791,7 @@ begin
end; end;
// DUC TX frequency (bytes 329-332) // DUC TX frequency (bytes 329-332)
Ph := FreqToPhaseWord(FCurrentTXFreq); Ph := FreqToPhaseWord(CalFreq(FCurrentTXFreq));
Buf[329] := (Ph shr 24) and $FF; Buf[329] := (Ph shr 24) and $FF;
Buf[330] := (Ph shr 16) and $FF; Buf[330] := (Ph shr 16) and $FF;
Buf[331] := (Ph shr 8) and $FF; Buf[331] := (Ph shr 8) and $FF;
+65 -6
View File
@@ -400,7 +400,9 @@ function BoardSupportsSupplyCurrent(BoardType: Integer): Boolean;
// Convert raw ADC value to supply current in Amps (Board 5 / Orion MkII only) // Convert raw ADC value to supply current in Amps (Board 5 / Orion MkII only)
// Uses UserADC1 (bytes 55-56). Returns -1 for unsupported boards. // 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) // Convert raw ADC value to RF power in Watts (ANAN-100)
function ADCToWatts100(RawADC: Word): Double; 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) // Convert raw ADC value to RF power in Watts (ANAN-10)
function ADCToWatts10(RawADC: Word): Double; 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 implementation
procedure PackU32BE(Value: LongWord; out B: array of Byte; Offset: Integer); procedure PackU32BE(Value: LongWord; out B: array of Byte; Offset: Integer);
@@ -476,14 +488,15 @@ begin
Result := BoardType = 5; Result := BoardType = 5;
end; end;
function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer): Double; function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer;
VoffMV: Double; SensMVA: Double): Double;
begin begin
// Board 5 (Orion MkII): UserADC1, Vref=5V → mV = adc/4095*5000. // 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. Дефолты Voff=360мВ, Sens=120мВ/А
// Formula: amps = (mV - Voff) / Sens. // (Thetis, ACS712-style); переопределяются калибровкой.
if BoardType = 5 then if (BoardType = 5) and (SensMVA > 0) then
begin 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; if Result < 0.0 then Result := 0.0;
end else end else
Result := -1.0; Result := -1.0;
@@ -507,4 +520,50 @@ begin
Result := (V * V) / 0.09; Result := (V * V) / 0.09;
end; 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. end.
+10
View File
@@ -72,6 +72,14 @@ var
iio_channel_attr_write_double: function(chn: Piio_channel; attr: PAnsiChar; iio_channel_attr_write_double: function(chn: Piio_channel; attr: PAnsiChar;
val: cdouble): cint; cdecl; 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) ---- // ---- Debug-атрибуты устройства (debugfs) ----
// Нужны для выбора антенного разъёма у AD936x в режиме 1R1T: // Нужны для выбора антенного разъёма у AD936x в режиме 1R1T:
// 'adi,1rx-1tx-mode-use-rx-num'/'-tx-num' (какой физический канал кормит тракт) // '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 := Sym('iio_channel_attr_write');
iio_channel_attr_write_bool := Sym('iio_channel_attr_write_bool'); iio_channel_attr_write_bool := Sym('iio_channel_attr_write_bool');
iio_channel_attr_write_double := Sym('iio_channel_attr_write_double'); 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_read := Sym('iio_device_debug_attr_read');
iio_device_debug_attr_write := Sym('iio_device_debug_attr_write'); iio_device_debug_attr_write := Sym('iio_device_debug_attr_write');
iio_device_debug_attr_write_longlong := Sym('iio_device_debug_attr_write_longlong'); iio_device_debug_attr_write_longlong := Sym('iio_device_debug_attr_write_longlong');
+101 -7
View File
@@ -347,6 +347,11 @@ type
FPSPopup: TObject; // TPureSignalPopup (cast при использовании) FPSPopup: TObject; // TPureSignalPopup (cast при использовании)
PanelRXBlock: TPanel; PanelRXBlock: TPanel;
// Шаговый аттенюатор ADC (openHPSDR): плавные 0..31 дБ. На Pluto ряд
// скрыт — там усиление тракта живёт в блоке RF AGC.
LblAtt: TLabel;
LblAttVal: TLabel;
TrkAtt: TFlatSlider;
BtnChannels: TFlatButton; BtnChannels: TFlatButton;
FChannelsDropDown: TFlatDropDown; FChannelsDropDown: TFlatDropDown;
FChannelsForm: TObject; // TChannelsForm (cast при использовании) FChannelsForm: TObject; // TChannelsForm (cast при использовании)
@@ -698,6 +703,7 @@ type
function DefaultMicSource: TTXMicSource; function DefaultMicSource: TTXMicSource;
procedure ApplyMOX(Active: Boolean); procedure ApplyMOX(Active: Boolean);
procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double); procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double);
procedure OnCalibrationSettingsChange(const C: TCalibration);
procedure OnPlutoTxMaxAttChange(MaxAttDb: Double); procedure OnPlutoTxMaxAttChange(MaxAttDb: Double);
procedure OnAnt936xChange(const A: TAD936xAnt); // разъёмы AD936x (Antenna) procedure OnAnt936xChange(const A: TAD936xAnt); // разъёмы AD936x (Antenna)
procedure OnVHFCalSettingsChange(const VHFCal: array of Double); procedure OnVHFCalSettingsChange(const VHFCal: array of Double);
@@ -705,6 +711,8 @@ type
procedure BtnAGCModeClick(Sender: TObject); procedure BtnAGCModeClick(Sender: TObject);
procedure TrkAGCChange(Sender: TObject); procedure TrkAGCChange(Sender: TObject);
procedure TrkRxGainChange(Sender: TObject); procedure TrkRxGainChange(Sender: TObject);
procedure TrkAttChange(Sender: TObject);
procedure UpdateAttLabel;
// (BtnPlutoGainClick объявлен выше) // (BtnPlutoGainClick объявлен выше)
procedure MeterTimerTick(Sender: TObject); procedure MeterTimerTick(Sender: TObject);
procedure SpectrumTimerTick(Sender: TObject); procedure SpectrumTimerTick(Sender: TObject);
@@ -761,6 +769,13 @@ uses SettingsForm;
{$R *.lfm} {$R *.lfm}
const
// Высота RX-блока левой панели: с рядом ATT (openHPSDR) и без него (Pluto —
// там усиление тракта в блоке RF AGC). Переключает LayoutLeftPanel, он же
// двигает панели ниже.
RX_BLOCK_H = 96;
RX_BLOCK_H_ATT = 120;
function TMainForm.LeftPanelButtonWidth(PanelWidth, ColCount, function TMainForm.LeftPanelButtonWidth(PanelWidth, ColCount,
ColIndex: Integer): Integer; ColIndex: Integer): Integer;
var var
@@ -1504,6 +1519,12 @@ begin
TrkVolume.Position := FController.ActiveVolume; // RX или TX-монитор TrkVolume.Position := FController.ActiveVolume; // RX или TX-монитор
rfRxGain: rfRxGain:
UpdateRxGainUI; UpdateRxGainUI;
rfAtten:
if TrkAtt <> nil then
begin
TrkAtt.Position := FController.FAtten;
UpdateAttLabel;
end;
rfDrive: rfDrive:
begin begin
TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive% TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive%
@@ -2027,7 +2048,7 @@ begin
// RX block: CTUN + VOL + NR/NB/SNB/ANF/MUTE // RX block: CTUN + VOL + NR/NB/SNB/ANF/MUTE
PanelRXBlock := TPanel.Create(Self); PanelRXBlock := TPanel.Create(Self);
PanelRXBlock.Parent := PanelLeft; PanelRXBlock.Parent := PanelLeft;
PanelRXBlock.SetBounds(0, Y, LEFT_W, 96); PanelRXBlock.SetBounds(0, Y, LEFT_W, RX_BLOCK_H_ATT);
PanelRXBlock.BevelOuter := bvNone; PanelRXBlock.BevelOuter := bvNone;
MakeLbl(PanelRXBlock, 'RX', 4, 2); MakeLbl(PanelRXBlock, 'RX', 4, 2);
@@ -2083,7 +2104,29 @@ begin
LeftPanelButtonLeft(LEFT_W, 5, 4), 70, LeftPanelButtonLeft(LEFT_W, 5, 4), 70,
LeftPanelButtonWidth(LEFT_W, 5, 4), BTN_SM, BtnMuteClick); LeftPanelButtonWidth(LEFT_W, 5, 4), BTN_SM, BtnMuteClick);
Inc(Y, 100); // Шаговый аттенюатор ADC — плавный 0..31 дБ (железо openHPSDR так и умеет,
// ступени 0/10/20 были только в web-UI). Показания S-метра/спектра на эти
// дБ компенсируются в контроллере, шкала dBm остаётся абсолютной.
LblAtt := TLabel.Create(Self);
LblAtt.Parent := PanelRXBlock; LblAtt.Left := 4; LblAtt.Top := 106;
LblAtt.Caption := 'ATT'; LblAtt.Font.Color := CLR_TEXTDIM;
LblAtt.Font.Name := 'Courier New'; LblAtt.Font.Size := 7;
TrkAtt := TFlatSlider.Create(Self);
TrkAtt.Parent := PanelRXBlock; TrkAtt.Left := 34; TrkAtt.Top := 102;
TrkAtt.Width := LEFT_W - 76; TrkAtt.Height := 16;
TrkAtt.Min := 0; TrkAtt.Max := 31; // ADC step attenuator, дБ
TrkAtt.Position := FController.FAtten;
TrkAtt.OnChange := TrkAttChange;
LblAttVal := TLabel.Create(Self);
LblAttVal.Parent := PanelRXBlock;
LblAttVal.Left := LEFT_W - 38; LblAttVal.Top := 106;
LblAttVal.Font.Color := CLR_TEXT; LblAttVal.Font.Name := 'Courier New';
LblAttVal.Font.Size := 7;
UpdateAttLabel;
Inc(Y, RX_BLOCK_H_ATT + 4);
// AGC controls // AGC controls
PanelAGC := TPanel.Create(Self); PanelAGC := TPanel.Create(Self);
@@ -3408,6 +3451,7 @@ procedure TMainForm.SpectrumTimerTick(Sender: TObject);
var var
NowTick: QWord; NowTick: QWord;
ElapsedTick: QWord; ElapsedTick: QWord;
LastPktTick: QWord;
I: Integer; I: Integer;
begin begin
// «×» шапки пана: закрытие отложено сюда (нельзя free панель из её кнопки). // «×» шапки пана: закрытие отложено сюда (нельзя free панель из её кнопки).
@@ -3437,8 +3481,12 @@ begin
begin begin
NowTick := GetTickCount64; NowTick := GetTickCount64;
ElapsedTick := NowTick - FController.FRXStartTime; ElapsedTick := NowTick - FController.FRXStartTime;
// Сетевой поток может обновить FRXLastPktTime на 1 ms ПОСЛЕ NowTick.
// QWord-вычитание в таком случае underflow'ится в огромное значение и
// давало ложный "Connection lost" при совершенно живом DDC-потоке.
LastPktTick := FController.FRXLastPktTime;
// Ждём первый пакет 5 секунд после старта // Ждём первый пакет 5 секунд после старта
if (FController.FRXLastPktTime = 0) and (ElapsedTick > 5000) then if (LastPktTick = 0) and (ElapsedTick > 5000) then
begin begin
BtnStartStop.Caption := 'START'; BtnStartStop.Caption := 'START';
StyleButton(BtnStartStop, False); StyleButton(BtnStartStop, False);
@@ -3450,7 +3498,8 @@ begin
Exit; Exit;
end; end;
// После первого пакета — следим чтобы поток не прерывался более 3 сек // После первого пакета — следим чтобы поток не прерывался более 3 сек
if (FController.FRXLastPktTime > 0) and ((NowTick - FController.FRXLastPktTime) > 3000) then if (LastPktTick > 0) and (NowTick >= LastPktTick) and
((NowTick - LastPktTick) > 3000) then
begin begin
BtnStartStop.Caption := 'START'; BtnStartStop.Caption := 'START';
StyleButton(BtnStartStop, False); StyleButton(BtnStartStop, False);
@@ -3474,7 +3523,7 @@ begin
if FController.FWDSPReady then if FController.FWDSPReady then
begin begin
FController.FLastSMeter := FController.FDSPEngine.GetSMeterDBm; FController.FLastSMeter := FController.ReadSMeterDBm;
FController.FDSPEngine.SetSpectrumWidth(FPan.SpectrumWidth); FController.FDSPEngine.SetSpectrumWidth(FPan.SpectrumWidth);
Inc(FAgcLineCounter); Inc(FAgcLineCounter);
if FAgcLineCounter >= 6 then if FAgcLineCounter >= 6 then
@@ -7413,6 +7462,12 @@ begin
UpdateDriveLabel; // изменился потолок → пересчитать дБ-подпись drive UpdateDriveLabel; // изменился потолок → пересчитать дБ-подпись drive
end; end;
procedure TMainForm.OnCalibrationSettingsChange(const C: TCalibration);
begin
// Логика (санитизация/офсет S-метра в DSP/persist) — в контроллере.
FController.SetCalibration(C);
end;
procedure TMainForm.OnAnt936xChange(const A: TAD936xAnt); procedure TMainForm.OnAnt936xChange(const A: TAD936xAnt);
// Вкладка Antenna (AD936x): таблица «диапазон → разъём». Контроллер сохраняет её // Вкладка Antenna (AD936x): таблица «диапазон → разъём». Контроллер сохраняет её
// и сразу применяет разъёмы текущего диапазона/трансвертера. // и сразу применяет разъёмы текущего диапазона/трансвертера.
@@ -7920,6 +7975,19 @@ begin
FController.SetRxGain(TrkRxGain.Position); // переводит в manual FController.SetRxGain(TrkRxGain.Position); // переводит в manual
end; end;
procedure TMainForm.TrkAttChange(Sender: TObject);
begin
if FSyncingFromController then Exit;
FController.SetAtten(TrkAtt.Position);
UpdateAttLabel;
end;
procedure TMainForm.UpdateAttLabel;
begin
if LblAttVal <> nil then
LblAttVal.Caption := Format('%ddB', [FController.FAtten]);
end;
procedure TMainForm.BtnPlutoGainClick(Sender: TObject); procedure TMainForm.BtnPlutoGainClick(Sender: TObject);
begin begin
if FSyncingFromController then Exit; if FSyncingFromController then Exit;
@@ -7936,10 +8004,20 @@ begin
for i := 0 to 3 do for i := 0 to 3 do
if BtnPlutoGain[i] <> nil then if BtnPlutoGain[i] <> nil then
StyleButton(BtnPlutoGain[i], BtnPlutoGain[i].Tag = FController.FRxGainMode); StyleButton(BtnPlutoGain[i], BtnPlutoGain[i].Tag = FController.FRxGainMode);
TrkRxGain.Position := FController.FRxGainDb;
TrkRxGain.Enabled := (FController.FRxGainMode = 0); // активен только в manual TrkRxGain.Enabled := (FController.FRxGainMode = 0); // активен только в manual
if FController.FRxGainMode = 0 then if FController.FRxGainMode = 0 then
LblRxGain.Caption := Format('%ddB', [FController.FRxGainDb]) begin
TrkRxGain.Position := FController.FRxGainDb;
LblRxGain.Caption := Format('%ddB', [FController.FRxGainDb]);
end
else if FController.FLastPlutoGainDb > TELEMETRY_NONE then
begin
// AGC: показываем ФАКТИЧЕСКОЕ усиление железа (опрос ~2 Гц) — ползунок
// ездит сам, тильда отличает измеренное от заданного. На это же значение
// компенсируются уровни (FrontEndCalDB), поэтому шкала dBm стоит.
TrkRxGain.Position := EnsureRange(Round(FController.FLastPlutoGainDb), 0, 73);
LblRxGain.Caption := Format('~%ddB', [Round(FController.FLastPlutoGainDb)]);
end
else else
LblRxGain.Caption := 'AGC'; LblRxGain.Caption := 'AGC';
finally finally
@@ -7953,6 +8031,17 @@ procedure TMainForm.LayoutLeftPanel(Pluto: Boolean);
var baseTop: Integer; var baseTop: Integer;
begin begin
if (PanelPlutoAGC = nil) or (PanelAGC = nil) or (PanelBands = nil) then Exit; if (PanelPlutoAGC = nil) or (PanelAGC = nil) or (PanelBands = nil) then Exit;
// Шаговый аттенюатор — только у openHPSDR; у AD936x усиление тракта в блоке
// RF AGC ниже, поэтому ряд ATT убираем и RX-блок ужимаем.
if (TrkAtt <> nil) and (LblAtt <> nil) and (LblAttVal <> nil) then
begin
TrkAtt.Visible := not Pluto;
LblAtt.Visible := not Pluto;
LblAttVal.Visible := not Pluto;
if Pluto then PanelRXBlock.Height := RX_BLOCK_H
else PanelRXBlock.Height := RX_BLOCK_H_ATT;
PanelAGC.Top := PanelRXBlock.Top + PanelRXBlock.Height + 4;
end;
baseTop := PanelAGC.Top + PanelAGC.Height + 4; baseTop := PanelAGC.Top + PanelAGC.Height + 4;
PanelPlutoAGC.Top := baseTop; PanelPlutoAGC.Top := baseTop;
PanelPlutoAGC.Visible := Pluto; PanelPlutoAGC.Visible := Pluto;
@@ -8226,6 +8315,7 @@ begin
SF.OnVisibilityChange := ApplyVisibility; SF.OnVisibilityChange := ApplyVisibility;
SF.OnFPSChange := ApplyFPS; SF.OnFPSChange := ApplyFPS;
SF.OnPAChange := OnPASettingsChange; SF.OnPAChange := OnPASettingsChange;
SF.OnCalibrationChange := OnCalibrationSettingsChange;
SF.OnPlutoTxChange := OnPlutoTxMaxAttChange; SF.OnPlutoTxChange := OnPlutoTxMaxAttChange;
SF.OnAnt936xChange := OnAnt936xChange; SF.OnAnt936xChange := OnAnt936xChange;
SF.OnVHFCalChange := OnVHFCalSettingsChange; SF.OnVHFCalChange := OnVHFCalSettingsChange;
@@ -8248,9 +8338,13 @@ begin
// физических разъёмов на диапазон/трансвертер (если разъёмов две пары). // физических разъёмов на диапазон/трансвертер (если разъёмов две пары).
SF.SetAntennaBackend(FController.BackendCaps.Kind = bkPluto, SF.SetAntennaBackend(FController.BackendCaps.Kind = bkPluto,
FController.BackendCaps.HasAntSwitch); FController.BackendCaps.HasAntSwitch);
// Вкладка Calibration: band-план и состав групп — по активному железу
// (у AD936x нет моста детектора мощности и телеметрии питания).
SF.SetCalibrationBackend(FController.BackendCaps.Kind = bkPluto);
SF.LoadAnt936x(FController.Ant936x, FController.FXvtrSettings); SF.LoadAnt936x(FController.Ant936x, FController.FXvtrSettings);
SF.LoadXvtrSettings(FController.FXvtrSettings); SF.LoadXvtrSettings(FController.FXvtrSettings);
SF.LoadPASettings(FController.FPAMaxPower, FController.FPABandCal); SF.LoadPASettings(FController.FPAMaxPower, FController.FPABandCal);
SF.LoadCalibration(FController.FCal);
SF.LoadPlutoTxMaxAtt(FController.FPlutoTxMaxAttDb); SF.LoadPlutoTxMaxAtt(FController.FPlutoTxMaxAttDb);
SF.LoadVHFCalSettings(FController.FVHFBandCal); SF.LoadVHFCalSettings(FController.FVHFBandCal);
SF.LoadCATSettings( SF.LoadCATSettings(
+74 -4
View File
@@ -46,6 +46,8 @@ type
FTXFreq: Double; FTXFreq: Double;
FSampleRate: Integer; FSampleRate: Integer;
FTransmitting: Boolean; FTransmitting: Boolean;
FFreqCalPPM: Double; // частотная калибровка опорника, ppm (xo_correction)
FXoNomHz: Int64; // номинал XO, Гц (снимается при коннекте, 0 = не знаем)
FGainMode: string; // gain_control_mode (manual/slow_attack/...) FGainMode: string; // gain_control_mode (manual/slow_attack/...)
FGainDb: Integer; // hardwaregain при ручном режиме FGainDb: Integer; // hardwaregain при ручном режиме
@@ -109,13 +111,16 @@ type
FTeleTemp: Double; // TELEMETRY_NONE, пока не прочитано FTeleTemp: Double; // TELEMETRY_NONE, пока не прочитано
FTeleSoc: Double; FTeleSoc: Double;
FTeleRssi: Double; FTeleRssi: Double;
FTeleGain: Double; // hardwaregain RX, дБ (в AGC-режимах крутит железо)
FTeleValid: Boolean; // был ли хоть один успешный опрос FTeleValid: Boolean; // был ли хоть один успешный опрос
procedure StartTelemetry; procedure StartTelemetry;
procedure StopTelemetry; procedure StopTelemetry;
// Блокирующий опрос железа: только из FTeleThread. // Блокирующий опрос железа: только из FTeleThread.
function PollTelemetryBlocking(out TempC, SocTempC, RSSIdB: Double): Boolean; function PollTelemetryBlocking(out TempC, SocTempC, RSSIdB,
GainDb: Double): Boolean;
// Низкоуровневые помощники применения атрибутов к ad9361-phy // Низкоуровневые помощники применения атрибутов к ad9361-phy
procedure ApplyFreqCal;
procedure ApplyRxLO(Hz: Double); procedure ApplyRxLO(Hz: Double);
procedure ApplyTxLO(Hz: Double); procedure ApplyTxLO(Hz: Double);
procedure SetTxLOPowerdown(PoweredOn: Boolean); // TX LO вкл/выкл (по PTT) procedure SetTxLOPowerdown(PoweredOn: Boolean); // TX LO вкл/выкл (по PTT)
@@ -174,6 +179,8 @@ type
procedure SendDUCIQ(const IData, QData: array of Integer); override; procedure SendDUCIQ(const IData, QData: array of Integer); override;
procedure ClearDUCIQQueue; override; procedure ClearDUCIQQueue; override;
function ReadTelemetry(out TempC, SocTempC, RSSIdB: Double): Boolean; override; function ReadTelemetry(out TempC, SocTempC, RSSIdB: Double): Boolean; override;
function RxGainNowDb: Double; override;
procedure SetFreqCalPPM(P: Double); override;
end; end;
// Стабильный 6-байтовый ключ (synthetic MAC) из serial/URI — для per-device // Стабильный 6-байтовый ключ (synthetic MAC) из serial/URI — для per-device
@@ -407,12 +414,14 @@ begin
FSampleRate := 1536000; FSampleRate := 1536000;
FGainMode := 'slow_attack'; // аппаратный AGC по умолчанию FGainMode := 'slow_attack'; // аппаратный AGC по умолчанию
FGainDb := 40; FGainDb := 40;
FFreqCalPPM := 0.0; FXoNomHz := 0;
FRxBuf := nil; FRxChI := nil; FRxChQ := nil; FRxThread := nil; FRxBuf := nil; FRxChI := nil; FRxChQ := nil; FRxThread := nil;
FStreaming := False; FRxSeq := 0; FStreaming := False; FRxSeq := 0;
FTxBuf := nil; FTxChI := nil; FTxChQ := nil; FTxThread := nil; FTxBuf := nil; FTxChI := nil; FTxChQ := nil; FTxThread := nil;
FTxStreaming := False; FTxRatio := 8; FTxStreaming := False; FTxRatio := 8;
FTeleThread := nil; FTeleValid := False; FTeleThread := nil; FTeleValid := False;
FTeleTemp := TELEMETRY_NONE; FTeleSoc := TELEMETRY_NONE; FTeleRssi := TELEMETRY_NONE; FTeleTemp := TELEMETRY_NONE; FTeleSoc := TELEMETRY_NONE; FTeleRssi := TELEMETRY_NONE;
FTeleGain := TELEMETRY_NONE;
InitCriticalSection(FTeleLock); InitCriticalSection(FTeleLock);
FTxAttDb := PLUTO_TX_OFF; // на старте TX выключен (макс. аттенюация) FTxAttDb := PLUTO_TX_OFF; // на старте TX выключен (макс. аттенюация)
// FIFO на ~4 TX-буфера запаса под джиттер продюсера/консьюмера. Окончательный // FIFO на ~4 TX-буфера запаса под джиттер продюсера/консьюмера. Окончательный
@@ -631,6 +640,7 @@ begin
ApplyTxMux; ApplyTxMux;
ApplyGain(True); ApplyGain(True);
ApplyTxAtten; // TX заглушён (макс. аттенюация) до явного PTT ApplyTxAtten; // TX заглушён (макс. аттенюация) до явного PTT
ApplyFreqCal; // частотная калибровка опорника (xo_correction)
StartTelemetry; StartTelemetry;
Result := True; Result := True;
end; end;
@@ -652,6 +662,40 @@ end;
// ---- Применение атрибутов к ad9361-phy ---- // ---- Применение атрибутов к 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); procedure TPlutoBackend.ApplyRxLO(Hz: Double);
var Ch: Piio_channel; var Ch: Piio_channel;
begin begin
@@ -1133,7 +1177,8 @@ begin
LeaveCriticalSection(FTxLock); LeaveCriticalSection(FTxLock);
end; end;
function TPlutoBackend.PollTelemetryBlocking(out TempC, SocTempC, RSSIdB: Double): Boolean; function TPlutoBackend.PollTelemetryBlocking(out TempC, SocTempC, RSSIdB,
GainDb: Double): Boolean;
// БЛОКИРУЮЩИЙ опрос железа — только из FTeleThread (каждое чтение = USB round-trip): // БЛОКИРУЮЩИЙ опрос железа — только из FTeleThread (каждое чтение = USB round-trip):
// ad9361-phy temp0/input — температура трансивера в миллиградусах C (longlong); // ad9361-phy temp0/input — температура трансивера в миллиградусах C (longlong);
// xadc temp0 — температура Zynq SoC/FPGA; сырой raw + offset (longlong) // xadc temp0 — температура Zynq SoC/FPGA; сырой raw + offset (longlong)
@@ -1149,6 +1194,7 @@ begin
TempC := TELEMETRY_NONE; TempC := TELEMETRY_NONE;
SocTempC := TELEMETRY_NONE; SocTempC := TELEMETRY_NONE;
RSSIdB := TELEMETRY_NONE; RSSIdB := TELEMETRY_NONE;
GainDb := TELEMETRY_NONE;
Result := False; Result := False;
if (not FConnected) or (FPhy = nil) or (not IIOLoaded) then Exit; if (not FConnected) or (FPhy = nil) or (not IIOLoaded) then Exit;
@@ -1190,6 +1236,29 @@ begin
RSSIdB := d; RSSIdB := d;
Result := True; Result := True;
end; end;
// hardwaregain: в режимах AGC железо крутит усиление само, и без него шкала
// dBm «дышит» вместе с ним (см. TRadioController.FrontEndCalDB).
if (Ch <> nil) and Assigned(iio_channel_attr_read_double)
and (iio_channel_attr_read_double(Ch, 'hardwaregain', d) = 0) then
begin
GainDb := d;
Result := True;
end;
end;
end;
function TPlutoBackend.RxGainNowDb: Double;
// В ручном режиме значение известно точно и без опроса железа; в AGC-режимах
// отдаём последний снимок телеметрии (обновляется ~2 Гц — быстрый AGC всё
// равно шевелится быстрее, для стабильной шкалы нужен MANUAL или SLOW).
begin
if FGainMode = 'manual' then Exit(FGainDb);
EnterCriticalSection(FTeleLock);
try
if FTeleValid and (FTeleGain > TELEMETRY_NONE) then Result := FTeleGain
else Result := FGainDb;
finally
LeaveCriticalSection(FTeleLock);
end; end;
end; end;
@@ -1244,7 +1313,7 @@ const
POLL_MS = 500; // ~2 Гц: чаще незачем, каждый опрос ~30 мс USB-транзакций POLL_MS = 500; // ~2 Гц: чаще незачем, каждый опрос ~30 мс USB-транзакций
STEP_MS = 25; // мелкий шаг сна — чтобы Terminate отрабатывал быстро STEP_MS = 25; // мелкий шаг сна — чтобы Terminate отрабатывал быстро
var var
t, s, r: Double; t, s, r, g: Double;
ok: Boolean; ok: Boolean;
Waited: Integer; Waited: Integer;
begin begin
@@ -1252,7 +1321,7 @@ begin
begin begin
if FBk.FConnected then if FBk.FConnected then
begin begin
ok := FBk.PollTelemetryBlocking(t, s, r); ok := FBk.PollTelemetryBlocking(t, s, r, g);
if ok then if ok then
begin begin
EnterCriticalSection(FBk.FTeleLock); EnterCriticalSection(FBk.FTeleLock);
@@ -1260,6 +1329,7 @@ begin
FBk.FTeleTemp := t; FBk.FTeleTemp := t;
FBk.FTeleSoc := s; FBk.FTeleSoc := s;
FBk.FTeleRssi := r; FBk.FTeleRssi := r;
FBk.FTeleGain := g;
FBk.FTeleValid := True; FBk.FTeleValid := True;
finally finally
LeaveCriticalSection(FBk.FTeleLock); LeaveCriticalSection(FBk.FTeleLock);
+17
View File
@@ -199,6 +199,14 @@ type
// RSSI приёмника (дБ). HPSDR — нет такой (использует HP-status), возвращает False. // RSSI приёмника (дБ). HPSDR — нет такой (использует HP-status), возвращает False.
// Недоступные значения = TELEMETRY_NONE. // Недоступные значения = TELEMETRY_NONE.
function ReadTelemetry(out TempC, SocTempC, RSSIdB: Double): Boolean; virtual; function ReadTelemetry(out TempC, SocTempC, RSSIdB: Double): Boolean; virtual;
// Текущее усиление приёмного тракта (дБ), если оно есть в железе и может
// меняться само: AD936x hardwaregain (в режимах AGC железо крутит его сам).
// Нужно для абсолютной шкалы уровней — контроллер вычитает его из показаний.
// 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; property Connected: Boolean read GetConnected;
@@ -300,6 +308,15 @@ begin
Result := False; Result := False;
end; end;
function TRadioBackend.RxGainNowDb: Double;
begin
Result := TELEMETRY_NONE;
end;
procedure TRadioBackend.SetFreqCalPPM(P: Double);
begin
end;
procedure TRadioBackend.SetTxAtten(Db: Double); procedure TRadioBackend.SetTxAtten(Db: Double);
begin begin
end; end;
+164 -14
View File
@@ -375,6 +375,10 @@ type
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings;
FPABandCal: array[0..CFG_BAND_COUNT-1] of Double; FPABandCal: array[0..CFG_BAND_COUNT-1] of Double;
FVHFBandCal: array[0..CFG_XVTR_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; FDevMAC: array[0..5] of Byte;
// Глобальные настройки устройства, загруженные ConnectDevice. Хранятся для // Глобальные настройки устройства, загруженные ConnectDevice. Хранятся для
// UI-рендера полей, которых нет в контроллере (DisplayFPS/FreqMhzDigits/CAT). // UI-рендера полей, которых нет в контроллере (DisplayFPS/FreqMhzDigits/CAT).
@@ -387,6 +391,9 @@ type
// RSSI приёмника дБ. Опрашиваются ServicePlutoTelemetry с UI-таймера; // RSSI приёмника дБ. Опрашиваются ServicePlutoTelemetry с UI-таймера;
// TELEMETRY_NONE = нет данных. // TELEMETRY_NONE = нет данных.
FLastPlutoTempC, FLastPlutoSocTempC, FLastPlutoRSSIdB: Double; FLastPlutoTempC, FLastPlutoSocTempC, FLastPlutoRSSIdB: Double;
// Фактическое усиление RX-тракта AD936x (дБ) — в режимах AGC его крутит
// железо. Снимается тем же опросом; UI показывает его в блоке RF AGC.
FLastPlutoGainDb: Double;
FPlutoTeleTick: QWord; // GetTickCount64 последнего опроса (троттлинг) FPlutoTeleTick: QWord; // GetTickCount64 последнего опроса (троттлинг)
FPlutoTempBad: Integer; // подряд отброшенных выбросов температуры (анти-залипание) FPlutoTempBad: Integer; // подряд отброшенных выбросов температуры (анти-залипание)
@@ -448,6 +455,10 @@ type
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer); procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnDemodAudioReady(const Left, Right: array of Single; Count: Integer); procedure OnDemodAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnDMRAudioReady(const PCM: array of Single; Count: Integer); procedure OnDMRAudioReady(const PCM: array of Single; Count: Integer);
// Общая выборка калибровочной таблицы «диапазон/трансвертер» по частоте.
function CalLookup(VisibleHz: Double;
const BandTbl: array of Double;
const XvtrTbl: array of Double): Double;
procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer); procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer); procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
procedure OnMicPacket(const Data: TMicDataPacket); procedure OnMicPacket(const Data: TMicDataPacket);
@@ -593,7 +604,7 @@ type
procedure ToggleMute; procedure ToggleMute;
procedure SetDrive(V: Integer); procedure SetDrive(V: Integer);
procedure DriveBy(Delta: Integer); procedure DriveBy(Delta: Integer);
procedure SetAtten(Idx: Integer); procedure SetAtten(Db: Integer); // ADC step attenuator, дБ (0..31)
// ---- Мультислайсы (софт-fan-out) ---- // ---- Мультислайсы (софт-fan-out) ----
// Создаёт слайс на TargetHz: заводит WDSP-канал в движке + свой аудио-выход // Создаёт слайс на TargetHz: заводит WDSP-канал в движке + свой аудио-выход
@@ -619,6 +630,17 @@ type
procedure SetSliceDSP(Id, NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean); procedure SetSliceDSP(Id, NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
procedure SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer); procedure SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer);
function SliceSMeter(Id: Integer): Double; // dBm, из DSP-потока function SliceSMeter(Id: Integer): Double; // dBm, из DSP-потока
// S-метр главного приёмника, dBm, с калибровочным офсетом текущего
// диапазона/трансвертера (движок отдаёт сырое значение).
function ReadSMeterDBm: Double;
// Офсет S-метра (дБ) для видимой частоты: слот трансвертера или диапазон.
function SMeterCalFor(VisibleHz: Double): Double;
// Офсет спектра/водопада (дБ) для дисплея: PanId 0 = главный, 1..N — пан.
// Колбэк движка (display-поток), см. TOnDispCalQuery.
function DispCalFor(PanId: Integer): Double;
// Поправка на регулируемое усиление входа (аттенюатор ADC у openHPSDR,
// hardwaregain у AD936x) — входит в оба офсета выше.
function FrontEndCalDB: Double;
procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string); procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string);
function SliceCount: Integer; function SliceCount: Integer;
function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean; function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
@@ -681,6 +703,7 @@ type
function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent
procedure ApplyPlutoTxAtten; // ставит att по FTransmitting/FTuning procedure ApplyPlutoTxAtten; // ставит att по FTransmitting/FTuning
procedure SetPlutoTxMaxAtt(Db: Double); // потолок на drive=100% (внешний усилитель) procedure SetPlutoTxMaxAtt(Db: Double); // потолок на drive=100% (внешний усилитель)
procedure SetCalibration(const C: TCalibration); // вкладка Calibration → FCal (офсеты S-метра — в SMeterCalFor)
procedure ApplyWDSPDrive(Pct: Integer); // цифровой drive WDSP: Pluto=1.0 (мощность — атт), HPSDR=Pct/100 procedure ApplyWDSPDrive(Pct: Integer); // цифровой drive WDSP: Pluto=1.0 (мощность — атт), HPSDR=Pct/100
// Диапазоны / трансвертеры / спан // Диапазоны / трансвертеры / спан
@@ -943,9 +966,11 @@ begin
FFMStepOn := True; FFMStepOn := True;
FRawFMDeviation := RAW_FM_DEV_DEFAULT; FRawFMDeviation := RAW_FM_DEV_DEFAULT;
FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно
TSettingsManager.DefaultCalibration(FCal);
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1; FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE; FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE;
FLastPlutoRSSIdB := TELEMETRY_NONE; FLastPlutoRSSIdB := TELEMETRY_NONE;
FLastPlutoGainDb := TELEMETRY_NONE;
FPlutoTeleTick := 0; FPlutoTempBad := 0; FPlutoTeleTick := 0; FPlutoTempBad := 0;
FillChar(FDevMAC, SizeOf(FDevMAC), 0); FillChar(FDevMAC, SizeOf(FDevMAC), 0);
end; end;
@@ -964,6 +989,9 @@ begin
FDSPEngine.OnSliceAudio := OnSliceAudioReady; FDSPEngine.OnSliceAudio := OnSliceAudioReady;
FDSPEngine.OnSliceDemodAudio := OnSliceDemodAudioReady; FDSPEngine.OnSliceDemodAudio := OnSliceDemodAudioReady;
FDSPEngine.OnDemodAudio := OnDemodAudioReady; FDSPEngine.OnDemodAudio := OnDemodAudioReady;
// Калибровка уровня спектра/водопада на диапазон/трансвертер — движок
// спрашивает её раз в кадр (сам он про диапазоны не знает).
FDSPEngine.OnDispCal := DispCalFor;
FDSPEngine.SetDisplayResLimit(FDisplayPixels); FDSPEngine.SetDisplayResLimit(FDisplayPixels);
// QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock). // QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
@@ -1186,10 +1214,12 @@ procedure TRadioController.OnHPStatus(const Status: THighPriorityStatus);
const const
SWR_MIN_FWD_W = 1.0; // ниже — fwd это ADC-шум, SWR не считаем SWR_MIN_FWD_W = 1.0; // ниже — fwd это ADC-шум, SWR не считаем
SWR_CAP = 9.9; SWR_CAP = 9.9;
BAND_6M = 10; // индекс 6m в HF-плане (160m..6m)
var var
FwdPwr, RevPwr: Word; FwdPwr, RevPwr: Word;
SupplyV, SupplyA, FwdW, SWRV, Rho: Double; SupplyV, SupplyA, FwdW, RevW, SWRV, Rho, DetK: Double;
IsTx: Boolean; Bi: Integer;
Is6m, IsTx: Boolean;
begin begin
FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo; FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo;
RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo; RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo;
@@ -1208,19 +1238,31 @@ begin
SupplyV := ADCToSupplyVolts( SupplyV := ADCToSupplyVolts(
(Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo, (Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo,
FNetwork.Device.BoardType); FNetwork.Device.BoardType);
if SupplyV >= 0 then SupplyV := SupplyV * FCal.SupplyVoltCal;
if BoardSupportsSupplyCurrent(FNetwork.Device.BoardType) then if BoardSupportsSupplyCurrent(FNetwork.Device.BoardType) then
SupplyA := ADCToSupplyCurrent( SupplyA := ADCToSupplyCurrent(
(Status.UserADC1Hi shl 8) or Status.UserADC1Lo, (Status.UserADC1Hi shl 8) or Status.UserADC1Lo,
FNetwork.Device.BoardType); FNetwork.Device.BoardType,
FCal.AmpVoffMV, FCal.AmpSensMVA);
end; 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). // FTransmitting — программный флаг; HPS_PTT — вход HW-PTT (footswitch/mic).
IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0); IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0);
// SWR = (1+ρ)/(1-ρ), ρ = adc_r/adc_f; только при значимой fwd-мощности. // SWR = (1+ρ)/(1-ρ), ρ = sqrt(RevW/FwdW) (Thetis); только при значимой
if IsTx and (FwdW >= SWR_MIN_FWD_W) and (FwdPwr > 0) then // fwd-мощности.
if IsTx and (FwdW >= SWR_MIN_FWD_W) then
begin begin
Rho := RevPwr / FwdPwr; Rho := Sqrt(RevW / FwdW);
if Rho >= 0.99 then if Rho >= 0.99 then
SWRV := SWR_CAP SWRV := SWR_CAP
else else
@@ -2018,9 +2060,78 @@ begin
end; end;
function TRadioController.SliceSMeter(Id: Integer): Double; function TRadioController.SliceSMeter(Id: Integer): Double;
// Слайс может стоять на другом диапазоне (паны N кросс-банд), поэтому офсет
// берём по ЕГО видимой частоте, а не по главному диапазону.
var idx: Integer;
begin begin
if Assigned(FDSPEngine) then Result := FDSPEngine.GetSliceSMeter(Id) Result := -130.0;
else Result := -130.0; if not Assigned(FDSPEngine) then Exit;
Result := FDSPEngine.GetSliceSMeter(Id);
idx := FindSliceIndex(Id);
if idx >= 0 then Result := Result + SMeterCalFor(FSlices[idx].TargetHz);
end;
function TRadioController.ReadSMeterDBm: Double;
begin
if not Assigned(FDSPEngine) then Exit(-130.0);
Result := FDSPEngine.GetSMeterDBm + SMeterCalFor(ActiveVfoHz);
end;
function TRadioController.CalLookup(VisibleHz: Double;
const BandTbl: array of Double; const XvtrTbl: array of Double): Double;
// Трансвертер имеет приоритет над диапазоном: его окно частот — видимые
// (не IF), и усиление тракта задаёт конвертер/LNB, а не КВ-вход трансивера.
// Индекс диапазона — по плану активного железа (HF openHPSDR / VHF-UHF Pluto).
var i, Bi: Integer;
begin
Result := 0.0;
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
and FXvtrSettings.Entries[FCurrentXvtr].Enabled
and (VisibleHz >= FXvtrSettings.Entries[FCurrentXvtr].FreqBegin)
and (VisibleHz <= FXvtrSettings.Entries[FCurrentXvtr].FreqEnd) then
Exit(XvtrTbl[FCurrentXvtr]);
for i := 0 to CFG_XVTR_COUNT-1 do
if FXvtrSettings.Entries[i].Enabled
and (VisibleHz >= FXvtrSettings.Entries[i].FreqBegin)
and (VisibleHz <= FXvtrSettings.Entries[i].FreqEnd) then
Exit(XvtrTbl[i]);
Bi := FreqToBandIdx(VisibleHz, IsPluto);
if (Bi >= 0) and (Bi < CFG_BAND_COUNT) then Result := BandTbl[Bi];
end;
function TRadioController.SMeterCalFor(VisibleHz: Double): Double;
begin
Result := CalLookup(VisibleHz, FCal.SMeterBandDB, FCal.SMeterXvtrDB)
+ FrontEndCalDB;
end;
function TRadioController.FrontEndCalDB: Double;
// Поправка на регулируемое усиление входного тракта — то, что меняется ручкой
// или само и не должно двигать абсолютную шкалу dBm.
// openHPSDR: шаговый аттенюатор ADC режет вход → его дБ возвращаем в
// показания (так же делают Thetis и piHPSDR).
// AD936x: hardwaregain поднимает уровень (а в режимах AGC железо крутит
// его само) → вычитаем текущее значение.
// Постоянная часть тракта (LNB, кабель, конвертер) остаётся за таблицами
// калибровки на диапазон/трансвертер.
var G: Double;
begin
if not IsPluto then Exit(FAtten);
Result := 0.0;
if FNetwork = nil then Exit;
G := FNetwork.RxGainNowDb;
if G > TELEMETRY_NONE then Result := -G;
end;
function TRadioController.DispCalFor(PanId: Integer): Double;
// Зовётся из display-потока движка раз в кадр. Главный дисплей калибруем по
// центру полосы, доп. пан — по своей частоте (паны бывают кросс-банд).
var Hz: Double;
begin
if (PanId > 0) and (PanId < MAX_PANS) then Hz := FPanFreqHz[PanId]
else Hz := FCenterFreq;
if Hz <= 0 then Exit(0.0);
Result := CalLookup(Hz, FCal.DispBandDB, FCal.DispXvtrDB) + FrontEndCalDB;
end; end;
procedure TRadioController.SetSliceDevice(Id, DevIndex: Integer; procedure TRadioController.SetSliceDevice(Id, DevIndex: Integer;
@@ -2184,7 +2295,9 @@ begin
// QO-100 / split-LO: TX идёт на собственный uplink-LO (visible - TxLOOffset), // QO-100 / split-LO: TX идёт на собственный uplink-LO (visible - TxLOOffset),
// без LOError — дрейф LNB к передаче отношения не имеет. // без LOError — дрейф LNB к передаче отношения не имеет.
if FXvtrSettings.Entries[FCurrentXvtr].FullDuplexTx then if FXvtrSettings.Entries[FCurrentXvtr].FullDuplexTx then
// TO Error: сам транспондер-offset держим стандартным, уход тракта — здесь.
Result := VisibleHz - FXvtrSettings.Entries[FCurrentXvtr].TxLOOffset Result := VisibleHz - FXvtrSettings.Entries[FCurrentXvtr].TxLOOffset
+ FXvtrSettings.Entries[FCurrentXvtr].TxLOError
else else
// Обычный трансвертер: RX=TX LO (с калибровкой LOError) — как раньше. // Обычный трансвертер: RX=TX LO (с калибровкой LOError) — как раньше.
Result := VisibleHz Result := VisibleHz
@@ -2723,6 +2836,7 @@ begin
// Аппаратный AGC AD936x — на диапазон (усиление тракта разное на 2м и QO-100). // Аппаратный AGC AD936x — на диапазон (усиление тракта разное на 2м и QO-100).
Result.RxGainMode := FRxGainMode; Result.RxGainMode := FRxGainMode;
Result.RxGainDb := FRxGainDb; Result.RxGainDb := FRxGainDb;
Result.AttenDB := FAtten; // шаговый аттенюатор ADC — тоже на диапазон
// Waterfall AGC/NF — глобальные (не диапазонные) // Waterfall AGC/NF — глобальные (не диапазонные)
end; end;
@@ -2761,6 +2875,7 @@ begin
FXvtrSettings.Entries[i].LastPanSlider := FPanSlider; FXvtrSettings.Entries[i].LastPanSlider := FPanSlider;
FXvtrSettings.Entries[i].LastRxGainMode := FRxGainMode; FXvtrSettings.Entries[i].LastRxGainMode := FRxGainMode;
FXvtrSettings.Entries[i].LastRxGainDb := FRxGainDb; FXvtrSettings.Entries[i].LastRxGainDb := FRxGainDb;
FXvtrSettings.Entries[i].LastAttenDB := FAtten;
end; end;
FSettings.SaveXvtr(FDevMAC, FXvtrSettings); FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
Exit; Exit;
@@ -2794,6 +2909,7 @@ begin
Result.PlutoTxMaxAttDb := FPlutoTxMaxAttDb; Result.PlutoTxMaxAttDb := FPlutoTxMaxAttDb;
for i := 0 to CFG_BAND_COUNT - 1 do Result.PABandCal[i] := FPABandCal[i]; 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]; for i := 0 to CFG_XVTR_COUNT - 1 do Result.VHFBandCal[i] := FVHFBandCal[i];
Result.Cal := FCal;
Result.ActiveVfo := FActiveVfo; Result.ActiveVfo := FActiveVfo;
Result.NRMode := FNRMode; Result.NRMode := FNRMode;
Result.NBMode := FNBMode; Result.NBMode := FNBMode;
@@ -2889,6 +3005,11 @@ begin
ApplyRxGainToBackend; ApplyRxGainToBackend;
Changed(rfRxGain); // UI: кнопки FAST/SLOW/HYB/MAN + ползунок Changed(rfRxGain); // UI: кнопки FAST/SLOW/HYB/MAN + ползунок
// --- Шаговый аттенюатор ADC (openHPSDR) — на диапазон/слот трансвертера.
FAtten := EnsureRange(B.AttenDB, 0, 31);
if Assigned(FNetwork) then FNetwork.SetStepAtten(Byte(FAtten));
Changed(rfAtten);
// --- FM squelch / CTCSS / step / repeater --- // --- FM squelch / CTCSS / step / repeater ---
FFMSQOn := B.FMSQOn; FFMSQOn := B.FMSQOn;
FFMSQLevel := B.FMSQLevel; FFMSQLevel := B.FMSQLevel;
@@ -3539,6 +3660,21 @@ begin
SaveGlobalSettings; // персист потолка (per-device) SaveGlobalSettings; // персист потолка (per-device)
end; 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;
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 внутри)
end;
procedure TRadioController.ApplyWDSPDrive(Pct: Integer); procedure TRadioController.ApplyWDSPDrive(Pct: Integer);
// На Pluto мощность задаётся ОДИН раз — железной аттенюацией (PlutoTxAttForDrive), // На Pluto мощность задаётся ОДИН раз — железной аттенюацией (PlutoTxAttForDrive),
// поэтому цифровой уровень WDSP держим на максимуме (1.0), а не режем по drive%. // поэтому цифровой уровень WDSP держим на максимуме (1.0), а не режем по drive%.
@@ -3549,12 +3685,20 @@ begin
else FDSPEngine.SetDriveLevel(EnsureRange(Pct, 0, 100) / 100.0); else FDSPEngine.SetDriveLevel(EnsureRange(Pct, 0, 100) / 100.0);
end; end;
procedure TRadioController.SetAtten(Idx: Integer); procedure TRadioController.SetAtten(Db: Integer);
// Шаговый аттенюатор ADC — плавный 0..31 дБ (как в железе openHPSDR), а не
// три фиксированные ступени. Уровни S-метра/спектра компенсируются на ту же
// величину (CalLookup), поэтому шкала dBm остаётся абсолютной.
begin begin
if Idx < 0 then Idx := 0 else if Idx > 2 then Idx := 2; FAtten := EnsureRange(Db, 0, 31);
FAtten := Idx; if Assigned(FNetwork) then FNetwork.SetStepAtten(Byte(FAtten));
if Assigned(FNetwork) then FNetwork.SetStepAtten(FAtten * 10);
Changed(rfAtten); Changed(rfAtten);
// Персист — вместе с диапазоном/слотом (SaveCurrentBand на выходе из них),
// писать JSON на каждый шаг ползунка незачем, как и у drive.
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
FXvtrSettings.Entries[FCurrentXvtr].LastAttenDB := FAtten
else if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
FBandCache[FCurrentBand].AttenDB := FAtten;
end; end;
procedure TRadioController.SetBand(Idx: Integer); procedure TRadioController.SetBand(Idx: Integer);
@@ -3645,6 +3789,7 @@ begin
B.FMRptOffsetHz := E.LastFMRptOffsetHz; B.FMRptOffsetHz := E.LastFMRptOffsetHz;
B.RxGainMode := E.LastRxGainMode; // hw-AGC слота (AD936x) B.RxGainMode := E.LastRxGainMode; // hw-AGC слота (AD936x)
B.RxGainDb := E.LastRxGainDb; B.RxGainDb := E.LastRxGainDb;
B.AttenDB := E.LastAttenDB; // аттенюатор ADC слота
B.VfoA := Vis; B.VfoA := Vis;
B.VfoB := FVfoB; B.VfoB := FVfoB;
ApplyBandDSP(B); ApplyBandDSP(B);
@@ -4921,6 +5066,7 @@ begin
FActiveDDC := 0; FActiveDDC := 0;
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE; FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE;
FLastPlutoRSSIdB := TELEMETRY_NONE; FLastPlutoRSSIdB := TELEMETRY_NONE;
FLastPlutoGainDb := TELEMETRY_NONE;
FPlutoTeleTick := 0; FPlutoTempBad := 0; FPlutoTeleTick := 0; FPlutoTempBad := 0;
Changed(rfRunning); // безусловный сброс run-кнопки/MOX.Enabled/sleep (как старый хвост STOP) Changed(rfRunning); // безусловный сброс run-кнопки/MOX.Enabled/sleep (как старый хвост STOP)
end; end;
@@ -5018,6 +5164,7 @@ begin
FPABandCal[i] := FLoadedGlobal.PABandCal[i]; FPABandCal[i] := FLoadedGlobal.PABandCal[i];
for i := 0 to CFG_XVTR_COUNT - 1 do for i := 0 to CFG_XVTR_COUNT - 1 do
FVHFBandCal[i] := FLoadedGlobal.VHFBandCal[i]; FVHFBandCal[i] := FLoadedGlobal.VHFBandCal[i];
SetCalibration(FLoadedGlobal.Cal);
// drive% (0..100) — раньше грузился через TrkDrive.Position; теперь напрямую, // drive% (0..100) — раньше грузился через TrkDrive.Position; теперь напрямую,
// слайдер выставит rfDevice-рендер. Калиброванный байт считаем после FCurrentBand. // слайдер выставит rfDevice-рендер. Калиброванный байт считаем после FCurrentBand.
FDrivePercent := EnsureRange(FLoadedGlobal.DriveLevel, 0, 100); FDrivePercent := EnsureRange(FLoadedGlobal.DriveLevel, 0, 100);
@@ -5378,6 +5525,9 @@ begin
// RSSI быстро меняется с сигналом — без сглаживания; обновляем, только если валиден. // RSSI быстро меняется с сигналом — без сглаживания; обновляем, только если валиден.
if r > TELEMETRY_NONE then FLastPlutoRSSIdB := r; if r > TELEMETRY_NONE then FLastPlutoRSSIdB := r;
// Фактическое усиление тракта (в MANUAL = заданное, в AGC — снятое с железа).
FLastPlutoGainDb := FNetwork.RxGainNowDb;
Changed(rfRxGain); // UI: живая цифра усиления в блоке RF AGC
Changed(rfSupply); // поле телеметрии статус-бара (поле 3); web-адаптер тоже Changed(rfSupply); // поле телеметрии статус-бара (поле 3); web-адаптер тоже
end; end;
+103 -1
View File
@@ -103,6 +103,9 @@ type
// сильно разное на 2м и на QO-100, поэтому режим и manual-гейн per-band. // сильно разное на 2м и на QO-100, поэтому режим и manual-гейн per-band.
RxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid RxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid
RxGainDb: Integer; // manual gain, дБ (0..73) RxGainDb: Integer; // manual gain, дБ (0..73)
// Шаговый аттенюатор ADC (openHPSDR) — на диапазон: на 40м ночью нужен,
// на 10м нет. 0..31 дБ.
AttenDB: Integer;
end; end;
// Персист панадаптеров и слайсов (секция "pans" под MAC). Восстанавливается // Персист панадаптеров и слайсов (секция "pans" под MAC). Восстанавливается
@@ -267,6 +270,10 @@ type
// собственный у SDR). FullDuplexTx помечает слот как такой транспондер. // собственный у SDR). FullDuplexTx помечает слот как такой транспондер.
FullDuplexTx: Boolean; // True — раздельный TX-LO (QO-100); авто display-duplex FullDuplexTx: Boolean; // True — раздельный TX-LO (QO-100); авто display-duplex
TxLOOffset: Double; // Hz, TX LO-offset (для QO-100 = транспондер-offset, ~8089.5 МГц) 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 тракта RXGain: Double; // dB, компенсация усиления RX тракта
RXOnly: Boolean; // True — запрещает TX на этом трансверторе RXOnly: Boolean; // True — запрещает TX на этом трансверторе
TXPower: Integer; // 0..100 % (drive level специально для XVTR) TXPower: Integer; // 0..100 % (drive level специально для XVTR)
@@ -295,6 +302,7 @@ type
// Аппаратный AGC AD936x на слот трансвертера (у QO-100 своё усиление тракта). // Аппаратный AGC AD936x на слот трансвертера (у QO-100 своё усиление тракта).
LastRxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid LastRxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid
LastRxGainDb: Integer; // manual gain, дБ LastRxGainDb: Integer; // manual gain, дБ
LastAttenDB: Integer; // шаговый аттенюатор ADC слота, дБ (0..31)
end; end;
TXvtrSettings = record TXvtrSettings = record
@@ -330,6 +338,34 @@ type
EnableXvtrHf: Boolean; // режим XVTR HF EnableXvtrHf: Boolean; // режим XVTR HF
end; 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)
// Офсет S-метра, дБ — раздельно на диапазон и на слот трансвертера (усиление
// тракта разное на 160м и на 10м, а у XVTR своё — конвертер/LNB). Смысл
// индекса диапазона задаёт band-план железа (HF у openHPSDR, VHF/UHF у
// Pluto); таблицы не пересекаются, т.к. секция JSON ключуется по MAC.
SMeterBandDB: array[0..CFG_BAND_COUNT-1] of Double;
SMeterXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double;
// Офсет спектра/водопада, дБ — та же разбивка. Отдельно от S-метра (как
// Display Cal Offset в Thetis): у display-тракта своё окно/детектор, и его
// уровень отличается от показаний стрелки на пару дБ.
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 TGlobalSettings = record
Volume: Integer; Volume: Integer;
TxMonVolume: Integer; // громкость self-monitor RX-аудио на TX (RX MUTE off) TxMonVolume: Integer; // громкость self-monitor RX-аудио на TX (RX MUTE off)
@@ -397,6 +433,8 @@ type
PlutoTxMaxAttDb: Double; // Pluto: TX-аттенюация на drive=100%, дБ<0 (дефолт -10) PlutoTxMaxAttDb: Double; // Pluto: TX-аттенюация на drive=100%, дБ<0 (дефолт -10)
PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0 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 VHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double; // калибровка для XVTR слотов 38.8..100.0
// --- Calibration (детектор мощности/КСВ, напряжение/ток, S-метр) ---
Cal: TCalibration;
// --- ADC settings --- // --- ADC settings ---
DitherEnabled: Boolean; // DDC Specific byte 5 — уменьшает интермодуляцию DitherEnabled: Boolean; // DDC Specific byte 5 — уменьшает интермодуляцию
RandomEnabled: Boolean; // DDC Specific byte 6 — рандомизация ADC RandomEnabled: Boolean; // DDC Specific byte 6 — рандомизация ADC
@@ -454,6 +492,7 @@ type
class function MacToStr(const MAC: array of Byte): string; class function MacToStr(const MAC: array of Byte): string;
class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings); class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings);
class procedure DefaultGlobal(out G: TGlobalSettings); class procedure DefaultGlobal(out G: TGlobalSettings);
class procedure DefaultCalibration(out C: TCalibration);
class procedure DefaultTX(out T: TTXSettings); class procedure DefaultTX(out T: TTXSettings);
// Размер окна — не привязан к MAC, хранится в корне JSON // Размер окна — не привязан к MAC, хранится в корне JSON
procedure SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean); procedure SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean);
@@ -533,6 +572,7 @@ begin
B.SpanHz := 192000; B.SpanHz := 192000;
B.RxGainMode := 2; // slow_attack (как глобальный дефолт Pluto) B.RxGainMode := 2; // slow_attack (как глобальный дефолт Pluto)
B.RxGainDb := 40; B.RxGainDb := 40;
B.AttenDB := 0;
B.FMSQOn := False; B.FMSQOn := False;
B.FMSQLevel := 30; B.FMSQLevel := 30;
B.CTCSSOn := False; B.CTCSSOn := False;
@@ -614,6 +654,19 @@ begin
// ADC — включены по умолчанию: поддерживаются на Hermes, Angelia, Orion // ADC — включены по умолчанию: поддерживаются на Hermes, Angelia, Orion
G.DitherEnabled := True; G.DitherEnabled := True;
G.RandomEnabled := 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;
// SMeterBandDB/SMeterXvtrDB — нули после FillChar (без коррекции).
end; end;
constructor TSettingsManager.Create(const FilePath: string); constructor TSettingsManager.Create(const FilePath: string);
@@ -741,7 +794,7 @@ end;
function TSettingsManager.LoadDevice(const MAC: array of Byte; function TSettingsManager.LoadDevice(const MAC: array of Byte;
out G: TGlobalSettings; out G: TGlobalSettings;
var Bands: array of TBandSettings): Boolean; var Bands: array of TBandSettings): Boolean;
var MacStr: string; DevObj,GObj,BObj: TJSONObject; i: Integer; var MacStr: string; DevObj,GObj,BObj: TJSONObject; i: Integer; SMLegacy: Double;
begin begin
MacStr := MacToStr(MAC); MacStr := MacToStr(MAC);
Result := FRoot.Find(MacStr) <> nil; Result := FRoot.Find(MacStr) <> nil;
@@ -856,6 +909,30 @@ begin
// ADC settings // ADC settings
G.DitherEnabled := JB(GObj,'dither_enabled', True); G.DitherEnabled := JB(GObj,'dither_enabled', True);
G.RandomEnabled := JB(GObj,'random_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);
// S-метр: офсет на диапазон/трансвертер. Легаси-ключ (одно значение на всё
// устройство) становится дефолтом всех слотов — показания не меняются.
SMLegacy := EnsureRange(JD(GObj,'cal_smeter_db', 0.0), -30.0, 30.0);
for i := 0 to CFG_BAND_COUNT-1 do
G.Cal.SMeterBandDB[i] :=
EnsureRange(JD(GObj,'cal_smeter_band_'+IntToStr(i), SMLegacy), -30.0, 30.0);
for i := 0 to CFG_XVTR_COUNT-1 do
G.Cal.SMeterXvtrDB[i] :=
EnsureRange(JD(GObj,'cal_smeter_xvtr_'+IntToStr(i), SMLegacy), -30.0, 30.0);
// Спектр/водопад: своя таблица офсетов (дефолт 0 — как было).
for i := 0 to CFG_BAND_COUNT-1 do
G.Cal.DispBandDB[i] :=
EnsureRange(JD(GObj,'cal_disp_band_'+IntToStr(i), 0.0), -30.0, 30.0);
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 for i := 0 to CFG_BAND_COUNT-1 do
begin begin
@@ -883,6 +960,7 @@ begin
Bands[i].FMRptOffsetHz := JD(BObj,'fmrpt_offset', Bands[i].FMRptOffsetHz); Bands[i].FMRptOffsetHz := JD(BObj,'fmrpt_offset', Bands[i].FMRptOffsetHz);
Bands[i].RxGainMode := EnsureRange(JI(BObj,'rx_gain_mode', Bands[i].RxGainMode), 0, 3); Bands[i].RxGainMode := EnsureRange(JI(BObj,'rx_gain_mode', Bands[i].RxGainMode), 0, 3);
Bands[i].RxGainDb := EnsureRange(JI(BObj,'rx_gain_db', Bands[i].RxGainDb), 0, 73); Bands[i].RxGainDb := EnsureRange(JI(BObj,'rx_gain_db', Bands[i].RxGainDb), 0, 73);
Bands[i].AttenDB := EnsureRange(JI(BObj,'atten_db', Bands[i].AttenDB), 0, 31);
end; end;
end; end;
@@ -965,6 +1043,22 @@ begin
// ADC settings // ADC settings
JW(O,'dither_enabled',G.DitherEnabled); JW(O,'dither_enabled',G.DitherEnabled);
JW(O,'random_enabled',G.RandomEnabled); 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);
for i := 0 to CFG_BAND_COUNT-1 do
JW(O,'cal_smeter_band_'+IntToStr(i),G.Cal.SMeterBandDB[i]);
for i := 0 to CFG_XVTR_COUNT-1 do
JW(O,'cal_smeter_xvtr_'+IntToStr(i),G.Cal.SMeterXvtrDB[i]);
for i := 0 to CFG_BAND_COUNT-1 do
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; end;
procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer; procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer;
@@ -984,6 +1078,7 @@ begin
JW(O,'fmrpt_dir',B.FMRptDir); JW(O,'fmrpt_offset',B.FMRptOffsetHz); JW(O,'fmrpt_dir',B.FMRptDir); JW(O,'fmrpt_offset',B.FMRptOffsetHz);
JW(O,'zoom_factor',B.ZoomFactor); JW(O,'pan_slider',B.PanSlider); JW(O,'zoom_factor',B.ZoomFactor); JW(O,'pan_slider',B.PanSlider);
JW(O,'rx_gain_mode',B.RxGainMode); JW(O,'rx_gain_db',B.RxGainDb); JW(O,'rx_gain_mode',B.RxGainMode); JW(O,'rx_gain_db',B.RxGainDb);
JW(O,'atten_db',B.AttenDB);
end; end;
function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer; function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer;
@@ -1015,6 +1110,7 @@ begin
B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz); B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz);
B.RxGainMode := EnsureRange(JI(O,'rx_gain_mode', B.RxGainMode), 0, 3); B.RxGainMode := EnsureRange(JI(O,'rx_gain_mode', B.RxGainMode), 0, 3);
B.RxGainDb := EnsureRange(JI(O,'rx_gain_db', B.RxGainDb), 0, 73); B.RxGainDb := EnsureRange(JI(O,'rx_gain_db', B.RxGainDb), 0, 73);
B.AttenDB := EnsureRange(JI(O,'atten_db', B.AttenDB), 0, 31);
B.ZoomFactor := EnsureRange(JD(O,'zoom_factor', B.ZoomFactor), 0.0, 1.0); B.ZoomFactor := EnsureRange(JD(O,'zoom_factor', B.ZoomFactor), 0.0, 1.0);
B.PanSlider := EnsureRange(JD(O,'pan_slider', B.PanSlider), 0.0, 1.0); B.PanSlider := EnsureRange(JD(O,'pan_slider', B.PanSlider), 0.0, 1.0);
end; end;
@@ -1514,6 +1610,7 @@ begin
X.Entries[i].LOError := 0.0; X.Entries[i].LOError := 0.0;
X.Entries[i].FullDuplexTx := False; X.Entries[i].FullDuplexTx := False;
X.Entries[i].TxLOOffset := 0.0; X.Entries[i].TxLOOffset := 0.0;
X.Entries[i].TxLOError := 0.0;
X.Entries[i].RXGain := 0.0; X.Entries[i].RXGain := 0.0;
X.Entries[i].RXOnly := False; X.Entries[i].RXOnly := False;
X.Entries[i].TXPower := 100; X.Entries[i].TXPower := 100;
@@ -1541,6 +1638,7 @@ begin
X.Entries[i].LastPanSlider := 0.5; X.Entries[i].LastPanSlider := 0.5;
X.Entries[i].LastRxGainMode := 2; // slow_attack X.Entries[i].LastRxGainMode := 2; // slow_attack
X.Entries[i].LastRxGainDb := 40; X.Entries[i].LastRxGainDb := 40;
X.Entries[i].LastAttenDB := 0;
end; end;
// Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились) // Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились)
X.Entries[0].ButtonText := '2m'; X.Entries[0].ButtonText := '2m';
@@ -1595,6 +1693,7 @@ begin
X.Entries[i].LOError := JD(EObj, 'lo_err', X.Entries[i].LOError); 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].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].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].RXGain := JD(EObj, 'rx_gain', X.Entries[i].RXGain);
X.Entries[i].RXOnly := JB(EObj, 'rx_only', X.Entries[i].RXOnly); 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); X.Entries[i].TXPower := EnsureRange(JI(EObj, 'tx_pwr', X.Entries[i].TXPower), 0, 100);
@@ -1622,6 +1721,7 @@ begin
X.Entries[i].LastPanSlider := EnsureRange(JD(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider), 0.0, 1.0); X.Entries[i].LastPanSlider := EnsureRange(JD(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider), 0.0, 1.0);
X.Entries[i].LastRxGainMode := EnsureRange(JI(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode), 0, 3); X.Entries[i].LastRxGainMode := EnsureRange(JI(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode), 0, 3);
X.Entries[i].LastRxGainDb := EnsureRange(JI(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb), 0, 73); X.Entries[i].LastRxGainDb := EnsureRange(JI(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb), 0, 73);
X.Entries[i].LastAttenDB := EnsureRange(JI(EObj, 'last_atten_db', X.Entries[i].LastAttenDB), 0, 31);
// Валидация: Last freqs должны быть в [FreqBegin..FreqEnd] // Валидация: Last freqs должны быть в [FreqBegin..FreqEnd]
if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or
(X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then (X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then
@@ -1695,6 +1795,7 @@ begin
JW(EObj, 'lo_err', X.Entries[i].LOError); JW(EObj, 'lo_err', X.Entries[i].LOError);
JW(EObj, 'full_dup', X.Entries[i].FullDuplexTx); JW(EObj, 'full_dup', X.Entries[i].FullDuplexTx);
JW(EObj, 'tx_lo_off', X.Entries[i].TxLOOffset); 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_gain', X.Entries[i].RXGain);
JW(EObj, 'rx_only', X.Entries[i].RXOnly); JW(EObj, 'rx_only', X.Entries[i].RXOnly);
JW(EObj, 'tx_pwr', X.Entries[i].TXPower); JW(EObj, 'tx_pwr', X.Entries[i].TXPower);
@@ -1722,6 +1823,7 @@ begin
JW(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider); JW(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider);
JW(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode); JW(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode);
JW(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb); JW(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb);
JW(EObj, 'last_atten_db', X.Entries[i].LastAttenDB);
end; end;
end; end;
+563 -8
View File
@@ -41,6 +41,8 @@ type
TOnAnt936xChange = procedure(const A: TAD936xAnt) of object; TOnAnt936xChange = procedure(const A: TAD936xAnt) of object;
// VHF (XVTR) PA calibration: 8 slots, 38.8..100.0 // VHF (XVTR) PA calibration: 8 slots, 38.8..100.0
TOnVHFCalChange = procedure(const VHFCal: array of Double) of object; TOnVHFCalChange = procedure(const VHFCal: array of Double) of object;
// Калибровки (вкладка Calibration): детектор мощности/КСВ, напряжение/ток, S-метр
TOnCalibrationChange = procedure(const C: TCalibration) of object;
// TX-настройки целиком (любая правка → один callback с актуальной структурой) // TX-настройки целиком (любая правка → один callback с актуальной структурой)
TOnTXSettingsChange = procedure(const T: TTXSettings) of object; TOnTXSettingsChange = procedure(const T: TTXSettings) of object;
@@ -150,6 +152,45 @@ type
FEdVHFCal: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit; FEdVHFCal: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit;
FVHFCalLabels: array[0..CFG_XVTR_COUNT-1] of TLabel; FVHFCalLabels: array[0..CFG_XVTR_COUNT-1] of TLabel;
// ---- Calibration tab controls ----
// Таблицы «на диапазон» строятся на 12 слотов (максимум плана); подписи,
// видимость и раскладка задаются железом в RelayoutCalTab: у openHPSDR
// виден HF-план, у Pluto — VHF/UHF-план, слоты трансвертеров — только
// включённые. Значения пишутся в per-device секцию JSON (ключ = MAC),
// поэтому таблицы разных аппаратов не пересекаются.
FPageCalib: TScrollBox;
FNavCalib: TFlatButton;
FCalGrpDet: TPanel; // Power / SWR detector
FCalGrpVA: TPanel; // Supply voltage / current
FCalGrpSM: TPanel; // S-meter
FCmbPwrDetModel: TFlatComboBox; // модель детектора Alex-моста
FEdPADetCal: array[0..CFG_BAND_COUNT-1] of TFlatFloatSpinEdit; // множитель FWD/REV на банд
FLblPADetCal: array[0..CFG_BAND_COUNT-1] of TLabel;
FEdSupplyVoltCal: TFlatFloatSpinEdit; // множитель напряжения питания
FEdAmpVoff: TFlatFloatSpinEdit; // датчик тока: сдвиг нуля, мВ
FEdAmpSens: TFlatFloatSpinEdit; // датчик тока: мВ/А
FEdSMBand: array[0..CFG_BAND_COUNT-1] of TFlatFloatSpinEdit; // офсет S-метра на диапазон
FLblSMBand: array[0..CFG_BAND_COUNT-1] of TLabel;
FEdSMXvtr: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit; // офсет S-метра на трансвертер
FLblSMXvtr: array[0..CFG_XVTR_COUNT-1] of TLabel;
FLblSMBandHdr: TLabel;
FLblSMXvtrHdr: TLabel;
FCalGrpDisp: TPanel; // Spectrum / waterfall
FEdDispBand: array[0..CFG_BAND_COUNT-1] of TFlatFloatSpinEdit; // офсет спектра на диапазон
FLblDispBand: array[0..CFG_BAND_COUNT-1] of TLabel;
FEdDispXvtr: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit; // офсет спектра на трансвертер
FLblDispXvtr: array[0..CFG_XVTR_COUNT-1] of TLabel;
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; // подпись слота
FCalConfig: TCalibration;
FOnCalibrationChange: TOnCalibrationChange;
// ---- Transmit tab controls ---- // ---- Transmit tab controls ----
// Текущее состояние, отдаётся в OnTXChange целиком при любой правке. // Текущее состояние, отдаётся в OnTXChange целиком при любой правке.
FTX: TTXSettings; FTX: TTXSettings;
@@ -277,6 +318,7 @@ type
FQoLnbLO: TFlatFloatSpinEdit; // МГц — LNB LO (= LOOffset, RX) FQoLnbLO: TFlatFloatSpinEdit; // МГц — LNB LO (= LOOffset, RX)
FQoLoErr: TFlatFloatSpinEdit; // Гц — LO Error (= LOError, живая калибровка LNB / beacon-lock) FQoLoErr: TFlatFloatSpinEdit; // Гц — LO Error (= LOError, живая калибровка LNB / beacon-lock)
FQoTxLO: TFlatFloatSpinEdit; // МГц — транспондер-offset (= TxLOOffset) FQoTxLO: TFlatFloatSpinEdit; // МГц — транспондер-offset (= TxLOOffset)
FQoTxLOErr: TFlatFloatSpinEdit; // Гц — поправка offset'а (TO Error)
FQoBegin: TFlatFloatSpinEdit; // МГц — downlink begin FQoBegin: TFlatFloatSpinEdit; // МГц — downlink begin
FQoEnd: TFlatFloatSpinEdit; // МГц — downlink end FQoEnd: TFlatFloatSpinEdit; // МГц — downlink end
FQoRXGain: TFlatFloatSpinEdit; // dB FQoRXGain: TFlatFloatSpinEdit; // dB
@@ -337,6 +379,19 @@ type
procedure BuildDisplayTab; procedure BuildDisplayTab;
procedure BuildRX1Tab; procedure BuildRX1Tab;
procedure BuildPATab; procedure BuildPATab;
procedure BuildCalibrationTab;
procedure RelayoutCalTab;
// Раскладка блока офсетов «на диапазон + на трансвертер» внутри группы
// (общая для S-метра и спектра). Возвращает нужную высоту группы.
function LayoutCalOffsets(const BandLbl: array of TLabel;
const BandEd: array of TFlatFloatSpinEdit;
XvtrHdr: TLabel;
const XvtrLbl: array of TLabel;
const XvtrEd: array of TFlatFloatSpinEdit;
Y0: Integer): Integer;
procedure OnCalDispFromSMClick(Sender: TObject);
procedure CollectCalFromUI;
procedure OnCalAnyChange(Sender: TObject);
procedure BuildCATTab; procedure BuildCATTab;
procedure BuildTransmitTab; procedure BuildTransmitTab;
procedure BuildAlexTab; procedure BuildAlexTab;
@@ -445,6 +500,9 @@ type
procedure LoadFreqMhzDigits(Digits: Integer); procedure LoadFreqMhzDigits(Digits: Integer);
procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double); procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double);
procedure LoadPlutoTxMaxAtt(MaxAttDb: Double); procedure LoadPlutoTxMaxAtt(MaxAttDb: Double);
procedure LoadCalibration(const C: TCalibration);
// Железо вкладки Calibration: band-план и состав групп (Pluto/openHPSDR).
procedure SetCalibrationBackend(IsPluto: Boolean);
// Вкладка Antenna: Alex-таблица (openHPSDR) либо таблица разъёмов AD936x. // Вкладка Antenna: Alex-таблица (openHPSDR) либо таблица разъёмов AD936x.
procedure SetAntennaBackend(IsAD936x, HasAntSwitch: Boolean); procedure SetAntennaBackend(IsAD936x, HasAntSwitch: Boolean);
// Таблица разъёмов + имена/видимость слотов трансвертеров. // Таблица разъёмов + имена/видимость слотов трансвертеров.
@@ -474,6 +532,7 @@ type
property OnPlutoTxChange: TOnPlutoTxChange read FOnPlutoTxChange write FOnPlutoTxChange; property OnPlutoTxChange: TOnPlutoTxChange read FOnPlutoTxChange write FOnPlutoTxChange;
property OnAnt936xChange: TOnAnt936xChange read FOnAnt936xChange write FOnAnt936xChange; property OnAnt936xChange: TOnAnt936xChange read FOnAnt936xChange write FOnAnt936xChange;
property OnVHFCalChange: TOnVHFCalChange read FOnVHFCalChange write FOnVHFCalChange; property OnVHFCalChange: TOnVHFCalChange read FOnVHFCalChange write FOnVHFCalChange;
property OnCalibrationChange: TOnCalibrationChange read FOnCalibrationChange write FOnCalibrationChange;
property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange; property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange;
property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange; property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange;
property OnWfRenderChange: TOnWfRenderChange read FOnWfRenderChange write FOnWfRenderChange; property OnWfRenderChange: TOnWfRenderChange read FOnWfRenderChange write FOnWfRenderChange;
@@ -550,6 +609,21 @@ const
PA_BAND_NAMES: array[0..10] of string = PA_BAND_NAMES: array[0..10] of string =
('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m'); ('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
// Раскладка вкладки Calibration — общая для построения (BuildCalibrationTab)
// и перекладки под активное железо (RelayoutCalTab).
CAL_MARGIN = 22;
CAL_PAD = 18;
CAL_ROW_H = 32;
CAL_R1 = 42;
CAL_ED_W = 72;
CAL_LBL_W = 54;
CAL_SM_LBL_W = 68; // подписи трансвертеров длиннее ('70cm', 'QO-100')
CAL_COLS = 4;
CAL_GRP_W = 700;
CAL_GRP1_H = 236; // Power / SWR detector
CAL_GRP2_H = 156; // Supply voltage / current
CAL_VA_LBL_W = 220;
{ TSettingsForm } { TSettingsForm }
constructor TSettingsForm.Create(AOwner: TComponent); constructor TSettingsForm.Create(AOwner: TComponent);
@@ -704,11 +778,12 @@ begin
FNavWaterfall := MakeNavButton('Waterfall', 182); FNavWaterfall := MakeNavButton('Waterfall', 182);
FNavTransmit := MakeNavButton('Transmit', 218); FNavTransmit := MakeNavButton('Transmit', 218);
FNavPA := MakeNavButton('Power Amplifier', 254); FNavPA := MakeNavButton('Power Amplifier', 254);
FNavAdvanced := MakeNavButton('Advanced', 290); FNavCalib := MakeNavButton('Calibration', 290);
FNavCAT := MakeNavButton('CAT', 326); FNavAdvanced := MakeNavButton('Advanced', 326);
FNavAlex := MakeNavButton('Antenna / Alex', 362); FNavCAT := MakeNavButton('CAT', 362);
FNavXvtr := MakeNavButton('Transverter', 398); FNavAlex := MakeNavButton('Antenna / Alex', 398);
FNavQO := MakeNavButton('QO-100', 434); FNavXvtr := MakeNavButton('Transverter', 434);
FNavQO := MakeNavButton('QO-100', 470);
FContentPanel := TPanel.Create(Self); FContentPanel := TPanel.Create(Self);
FContentPanel.Parent := Self; FContentPanel.Parent := Self;
@@ -722,6 +797,7 @@ begin
FPageWaterfall := MakeScrollPage; FPageWaterfall := MakeScrollPage;
FPageTransmit := MakeScrollPage; FPageTransmit := MakeScrollPage;
FPagePA := MakeScrollPage; FPagePA := MakeScrollPage;
FPageCalib := MakeScrollPage;
FPageAdvanced := MakeScrollPage; FPageAdvanced := MakeScrollPage;
FPageCAT := MakeScrollPage; FPageCAT := MakeScrollPage;
FPageAlex := MakeScrollPage; FPageAlex := MakeScrollPage;
@@ -730,12 +806,14 @@ begin
FAlexBoardType := 0; FAlexBoardType := 0;
TSettingsManager.DefaultAlex(FAlexConfig); TSettingsManager.DefaultAlex(FAlexConfig);
TSettingsManager.DefaultXvtr(FXvtrConfig); TSettingsManager.DefaultXvtr(FXvtrConfig);
TSettingsManager.DefaultCalibration(FCalConfig);
BuildAudioTab; BuildAudioTab;
BuildDisplayTab; BuildDisplayTab;
BuildRX1Tab; BuildRX1Tab;
BuildTransmitTab; BuildTransmitTab;
BuildPATab; BuildPATab;
BuildCalibrationTab;
BuildAdvancedTab; BuildAdvancedTab;
BuildCATTab; BuildCATTab;
BuildAlexTab; BuildAlexTab;
@@ -768,6 +846,7 @@ begin
else if Sender = FNavWaterfall then SelectPage(FPageWaterfall, FNavWaterfall) else if Sender = FNavWaterfall then SelectPage(FPageWaterfall, FNavWaterfall)
else if Sender = FNavTransmit then SelectPage(FPageTransmit, FNavTransmit) else if Sender = FNavTransmit then SelectPage(FPageTransmit, FNavTransmit)
else if Sender = FNavPA then SelectPage(FPagePA, FNavPA) 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 = 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) else if Sender = FNavAlex then SelectPage(FPageAlex, FNavAlex)
@@ -783,6 +862,7 @@ begin
FPageWaterfall.Visible:= APage = FPageWaterfall; FPageWaterfall.Visible:= APage = FPageWaterfall;
FPageTransmit.Visible := APage = FPageTransmit; FPageTransmit.Visible := APage = FPageTransmit;
FPagePA.Visible := APage = FPagePA; FPagePA.Visible := APage = FPagePA;
FPageCalib.Visible := APage = FPageCalib;
FPageAdvanced.Visible := APage = FPageAdvanced; FPageAdvanced.Visible := APage = FPageAdvanced;
FPageCAT.Visible := APage = FPageCAT; FPageCAT.Visible := APage = FPageCAT;
FPageAlex.Visible := APage = FPageAlex; FPageAlex.Visible := APage = FPageAlex;
@@ -795,6 +875,7 @@ begin
FNavWaterfall.Active:= ANav = FNavWaterfall; FNavWaterfall.Active:= ANav = FNavWaterfall;
FNavTransmit.Active := ANav = FNavTransmit; FNavTransmit.Active := ANav = FNavTransmit;
FNavPA.Active := ANav = FNavPA; FNavPA.Active := ANav = FNavPA;
FNavCalib.Active := ANav = FNavCalib;
FNavAdvanced.Active := ANav = FNavAdvanced; FNavAdvanced.Active := ANav = FNavAdvanced;
FNavCAT.Active := ANav = FNavCAT; FNavCAT.Active := ANav = FNavCAT;
FNavAlex.Active := ANav = FNavAlex; FNavAlex.Active := ANav = FNavAlex;
@@ -1859,6 +1940,469 @@ begin
end; end;
procedure TSettingsForm.BuildCalibrationTab;
// Вкладка Calibration (паритет с Thetis-Enhanced): модель детектора мощности
// Alex-моста + множитель FWD/REV на банд (Det. Cal.), калибровка напряжения/
// тока питания (PAVoltCal/AmpVoff/AmpSens) и офсет S-метра на диапазон и на
// слот трансвертера. Позиции/подписи/видимость band-строк ставит RelayoutCalTab
// под активное железо — здесь только создание контролов.
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;
var E: TFlatFloatSpinEdit;
begin
E := TFlatFloatSpinEdit.Create(Self);
E.Parent := AParent;
E.SetBounds(ALeft, ATop, CAL_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, GTop: Integer;
begin
// Гард: SetItemIndex у TFlatComboBox стреляет OnChange и при программном
// присвоении — без гарда OnCalAnyChange дёрнется из «ItemIndex := 0» ниже,
// когда остальные контролы вкладки ещё nil → AV в CollectCalFromUI.
FLoading := True;
FCalIsPluto := False;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
FCalXvtrOn[i] := False;
FCalXvtrName[i] := '';
end;
with TLabel.Create(Self) do
begin
Parent := FPageCalib;
Caption := 'Calibration';
SetBounds(CAL_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(CAL_MARGIN, 48, 620, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
// ── Power / SWR detector ────────────────────────────────────────────────
GTop := 86;
FCalGrpDet := MakeGroupPanel(FPageCalib, 'Power / SWR detector',
CAL_MARGIN, GTop, CAL_GRP_W, CAL_GRP1_H);
Grp := FCalGrpDet;
Lbl := MakeLbl(Grp, 'Detector model', CAL_PAD, CAL_R1 + 6, 132);
FCmbPwrDetModel := MakeCombo(Grp, CAL_PAD + 150, CAL_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.',
CAL_PAD, CAL_R1 + CAL_ROW_H + 8, 650);
WrapLbl(Lbl, 650, 34);
for i := 0 to CFG_BAND_COUNT - 1 do
begin
FLblPADetCal[i] := MakeLbl(Grp, '', 0, 0, CAL_LBL_W);
FEdPADetCal[i] := MakeCalSpin(Grp, 0, 0, 0.50, 2.00, 0.01, 2, 1.00);
FEdPADetCal[i].Tag := i;
end;
// ── Supply voltage / current ────────────────────────────────────────────
GTop := GTop + CAL_GRP1_H + 14;
FCalGrpVA := MakeGroupPanel(FPageCalib, 'Supply voltage / current',
CAL_MARGIN, GTop, CAL_GRP_W, CAL_GRP2_H);
Grp := FCalGrpVA;
Lbl := MakeLbl(Grp, 'Voltage multiplier (1.000 = no correction)',
CAL_PAD, CAL_R1 + 6, CAL_VA_LBL_W);
FEdSupplyVoltCal := MakeCalSpin(Grp, CAL_PAD + CAL_VA_LBL_W + 10, CAL_R1,
0.100, 5.000, 0.001, 3, 1.000);
Lbl := MakeLbl(Grp, 'Current sensor zero offset (mV)',
CAL_PAD, CAL_R1 + CAL_ROW_H + 6, CAL_VA_LBL_W);
FEdAmpVoff := MakeCalSpin(Grp, CAL_PAD + CAL_VA_LBL_W + 10, CAL_R1 + CAL_ROW_H,
0.0, 2000.0, 1.0, 0, 360.0);
Lbl := MakeLbl(Grp, 'Current sensor sensitivity (mV/A)',
CAL_PAD, CAL_R1 + 2 * CAL_ROW_H + 6, CAL_VA_LBL_W);
FEdAmpSens := MakeCalSpin(Grp, CAL_PAD + CAL_VA_LBL_W + 10, CAL_R1 + 2 * CAL_ROW_H,
1.0, 1000.0, 0.1, 1, 120.0);
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;
FCalGrpSM := MakeGroupPanel(FPageCalib, 'S-meter',
CAL_MARGIN, GTop, CAL_GRP_W, GRP3_H0);
Grp := FCalGrpSM;
FLblSMBandHdr := MakeLbl(Grp,
'Offset (dB) added to S-meter readings — per band (+ = meter reads higher).',
CAL_PAD, 30, 540);
WrapLbl(FLblSMBandHdr, 540, 18);
for i := 0 to CFG_BAND_COUNT - 1 do
begin
FLblSMBand[i] := MakeLbl(Grp, '', 0, 0, CAL_SM_LBL_W);
FEdSMBand[i] := MakeCalSpin(Grp, 0, 0, -30.0, 30.0, 0.1, 1, 0.0);
FEdSMBand[i].Tag := i;
end;
FLblSMXvtrHdr := MakeLbl(Grp, 'Per transverter slot', CAL_PAD, 0, 300);
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
FLblSMXvtr[i] := MakeLbl(Grp, '', 0, 0, CAL_SM_LBL_W);
FLblSMXvtr[i].AutoSize := False;
FLblSMXvtr[i].ShowHint := True;
FEdSMXvtr[i] := MakeCalSpin(Grp, 0, 0, -30.0, 30.0, 0.1, 1, 0.0);
FEdSMXvtr[i].Tag := i;
end;
// ── Spectrum / waterfall ────────────────────────────────────────────────
// Отдельная от S-метра калибровка (Display Cal Offset в Thetis): у display-
// тракта своё окно/детектор, поэтому уровень трассы и показания стрелки
// расходятся на пару дБ. Сдвиг двигает ВСЮ трассу (и шумовую полку, и
// сигналы) — это калибровка уровня, а не подавление шума.
FCalGrpDisp := MakeGroupPanel(FPageCalib, 'Spectrum / waterfall',
CAL_MARGIN, GTop, CAL_GRP_W, GRP3_H0);
Grp := FCalGrpDisp;
FLblDispBandHdr := MakeLbl(Grp,
'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;
FBtnDispFromSM.Caption := '= S-meter';
FBtnDispFromSM.SetBounds(CAL_GRP_W - 130, 24, 112, BTN_H);
FBtnDispFromSM.OnClick := OnCalDispFromSMClick;
FBtnDispFromSM.ClrNorm := CLR_PANEL;
FBtnDispFromSM.ClrBorder := TColor($00404040);
FBtnDispFromSM.ClrHot := TColor($00303030);
FBtnDispFromSM.ClrActive := TColor($00003300);
FBtnDispFromSM.ClrText := CLR_TEXT;
FBtnDispFromSM.ClrTextAct := CLR_ACCENT;
FBtnDispFromSM.Font.Name := UI_FONT;
FBtnDispFromSM.Font.Size := 9;
FBtnDispFromSM.Hint := 'Copy the S-meter offsets into this table';
FBtnDispFromSM.ShowHint := True;
for i := 0 to CFG_BAND_COUNT - 1 do
begin
FLblDispBand[i] := MakeLbl(Grp, '', 0, 0, CAL_SM_LBL_W);
FEdDispBand[i] := MakeCalSpin(Grp, 0, 0, -30.0, 30.0, 0.1, 1, 0.0);
FEdDispBand[i].Tag := i;
end;
FLblDispXvtrHdr := MakeLbl(Grp, 'Per transverter slot', CAL_PAD, 0, 300);
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
FLblDispXvtr[i] := MakeLbl(Grp, '', 0, 0, CAL_SM_LBL_W);
FLblDispXvtr[i].AutoSize := False;
FLblDispXvtr[i].ShowHint := True;
FEdDispXvtr[i] := MakeCalSpin(Grp, 0, 0, -30.0, 30.0, 0.1, 1, 0.0);
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;
end;
procedure TSettingsForm.RelayoutCalTab;
// Показывает только то, что есть у активного железа: band-план (HF у openHPSDR,
// VHF/UHF у Pluto), из трансвертеров — включённые слоты. Детектор мощности и
// датчики питания — свойства Alex-моста openHPSDR, у AD936x такой телеметрии
// нет, поэтому обе группы прячутся, а S-метр поднимается наверх.
var
i, r, c, X, Y, GTop, HSM, HDisp: Integer;
Nm: string;
begin
// Гард: вкладка ещё строится (или LoadXvtrSettings пришёл раньше неё).
if (FCalGrpDet = nil) or (FCalGrpVA = nil) or (FCalGrpSM = nil)
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
begin
Nm := BandName(i, FCalIsPluto);
r := i div CAL_COLS;
c := i mod CAL_COLS;
X := CAL_PAD + c * (CAL_LBL_W + CAL_ED_W + 18);
Y := CAL_R1 + CAL_ROW_H + 46 + r * CAL_ROW_H;
if FLblPADetCal[i] <> nil then
begin
FLblPADetCal[i].Caption := Nm + ':';
FLblPADetCal[i].SetBounds(X, Y + 5, CAL_LBL_W, 18);
FLblPADetCal[i].Visible := Nm <> '';
end;
if FEdPADetCal[i] <> nil then
begin
FEdPADetCal[i].SetBounds(X + CAL_LBL_W, Y, CAL_ED_W, BTN_H + 2);
FEdPADetCal[i].Visible := Nm <> '';
end;
end;
// ---- таблицы офсетов (S-метр и спектр) — одинаковая геометрия ----
HSM := LayoutCalOffsets(FLblSMBand, FEdSMBand, FLblSMXvtrHdr,
FLblSMXvtr, FEdSMXvtr, 52);
HDisp := LayoutCalOffsets(FLblDispBand, FEdDispBand, FLblDispXvtrHdr,
FLblDispXvtr, FEdDispXvtr, 52);
// ---- вертикальная укладка групп ----
GTop := 86;
FCalGrpDet.Visible := not FCalIsPluto;
FCalGrpVA.Visible := not FCalIsPluto;
if not FCalIsPluto then
begin
FCalGrpDet.SetBounds(CAL_MARGIN, GTop, CAL_GRP_W, CAL_GRP1_H);
GTop := GTop + CAL_GRP1_H + 14;
FCalGrpVA.SetBounds(CAL_MARGIN, GTop, CAL_GRP_W, CAL_GRP2_H);
GTop := GTop + CAL_GRP2_H + 14;
end;
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;
const BandEd: array of TFlatFloatSpinEdit; XvtrHdr: TLabel;
const XvtrLbl: array of TLabel; const XvtrEd: array of TFlatFloatSpinEdit;
Y0: Integer): Integer;
// Строки диапазонов стоят по индексу плана (пустой слот просто скрыт),
// слоты трансвертеров — только включённые и без дырок.
var
i, r, c, X, Y, Slot, BandRows, XvtrRows: Integer;
Nm: string;
begin
BandRows := 0;
for i := 0 to CFG_BAND_COUNT - 1 do
begin
Nm := BandName(i, FCalIsPluto);
r := i div CAL_COLS;
c := i mod CAL_COLS;
X := CAL_PAD + c * (CAL_SM_LBL_W + CAL_ED_W + 14);
Y := Y0 + r * CAL_ROW_H;
if BandLbl[i] <> nil then
begin
BandLbl[i].Caption := Nm + ':';
BandLbl[i].SetBounds(X, Y + 5, CAL_SM_LBL_W, 18);
BandLbl[i].Visible := Nm <> '';
end;
if BandEd[i] <> nil then
begin
BandEd[i].SetBounds(X + CAL_SM_LBL_W, Y, CAL_ED_W, BTN_H + 2);
BandEd[i].Visible := Nm <> '';
end;
if Nm <> '' then BandRows := r + 1;
end;
Y := Y0 + BandRows * CAL_ROW_H + 6;
XvtrHdr.SetBounds(CAL_PAD, Y, 300, 18);
Slot := 0;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
if FCalXvtrOn[i] then
begin
r := Slot div CAL_COLS;
c := Slot mod CAL_COLS;
X := CAL_PAD + c * (CAL_SM_LBL_W + CAL_ED_W + 14);
if FCalXvtrName[i] <> '' then Nm := FCalXvtrName[i]
else Nm := 'XVTR' + IntToStr(i);
XvtrLbl[i].Caption := Nm + ':';
XvtrLbl[i].Hint := 'XVTR' + IntToStr(i) + ' (' + Nm + ')';
XvtrLbl[i].SetBounds(X, Y + 24 + r * CAL_ROW_H + 5, CAL_SM_LBL_W, 18);
XvtrEd[i].SetBounds(X + CAL_SM_LBL_W, Y + 24 + r * CAL_ROW_H,
CAL_ED_W, BTN_H + 2);
Inc(Slot);
end;
XvtrLbl[i].Visible := FCalXvtrOn[i];
XvtrEd[i].Visible := FCalXvtrOn[i];
end;
XvtrRows := (Slot + CAL_COLS - 1) div CAL_COLS;
XvtrHdr.Visible := Slot > 0;
if Slot > 0 then Result := Y + 24 + XvtrRows * CAL_ROW_H + 12
else Result := Y + 12;
end;
procedure TSettingsForm.OnCalDispFromSMClick(Sender: TObject);
// Стартовая точка калибровки дисплея: те же цифры, что у стрелки,
// дальше доводятся руками по шумовой полке/эталонному генератору.
var i: Integer;
begin
FLoading := True;
try
for i := 0 to CFG_BAND_COUNT - 1 do
if (FEdDispBand[i] <> nil) and (FEdSMBand[i] <> nil) then
FEdDispBand[i].Value := FEdSMBand[i].Value;
for i := 0 to CFG_XVTR_COUNT - 1 do
if (FEdDispXvtr[i] <> nil) and (FEdSMXvtr[i] <> nil) then
FEdDispXvtr[i].Value := FEdSMXvtr[i].Value;
finally
FLoading := False;
end;
OnCalAnyChange(FBtnDispFromSM);
end;
procedure TSettingsForm.SetCalibrationBackend(IsPluto: Boolean);
begin
FCalIsPluto := IsPluto;
RelayoutCalTab;
end;
procedure TSettingsForm.CollectCalFromUI;
var i: Integer;
begin
FCalConfig.PwrDetModel := FCmbPwrDetModel.ItemIndex;
for i := 0 to CFG_BAND_COUNT - 1 do
begin
if FEdPADetCal[i] <> nil then FCalConfig.PADetCal[i] := FEdPADetCal[i].Value;
if FEdSMBand[i] <> nil then FCalConfig.SMeterBandDB[i] := FEdSMBand[i].Value;
if FEdDispBand[i] <> nil then FCalConfig.DispBandDB[i] := FEdDispBand[i].Value;
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
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;
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 CFG_BAND_COUNT - 1 do
begin
if FEdPADetCal[i] <> nil then FEdPADetCal[i].Value := C.PADetCal[i];
if FEdSMBand[i] <> nil then FEdSMBand[i].Value := C.SMeterBandDB[i];
if FEdDispBand[i] <> nil then FEdDispBand[i].Value := C.DispBandDB[i];
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
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;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.BuildAdvancedTab; procedure TSettingsForm.BuildAdvancedTab;
const const
MARGIN = 22; MARGIN = 22;
@@ -3801,7 +4345,11 @@ begin
FVHFCalLabels[i].Caption := 'VHF' + IntToStr(i) + ':'; FVHFCalLabels[i].Caption := 'VHF' + IntToStr(i) + ':';
FVHFCalLabels[i].Hint := FVHFCalLabels[i].Caption; FVHFCalLabels[i].Hint := FVHFCalLabels[i].Caption;
end; end;
// Офсеты S-метра на вкладке Calibration — только по включённым слотам.
FCalXvtrOn[i] := X.Entries[i].Enabled;
FCalXvtrName[i] := X.Entries[i].ButtonText;
end; end;
RelayoutCalTab;
if FChkXvtrTunPwr <> nil then FChkXvtrTunPwr.Checked := X.UseXVTRTunePower; if FChkXvtrTunPwr <> nil then FChkXvtrTunPwr.Checked := X.UseXVTRTunePower;
LoadQOFromConfig; // QO100_SLOT → отдельная страница QO-100 LoadQOFromConfig; // QO100_SLOT → отдельная страница QO-100
finally finally
@@ -3859,7 +4407,7 @@ begin
L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM; L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM;
L.WordWrap := True; 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; Y := GRP_TITLE_H + 8;
FQoEn := TFlatCheckBox.Create(Self); FQoEn := TFlatCheckBox.Create(Self);
@@ -3881,6 +4429,11 @@ begin
FQoTxLO := QoFloat(Y, 0.0, 50000.0, 0.001, 6); FQoTxLO := QoFloat(Y, 0.0, 50000.0, 0.001, 6);
FQoTxLO.Value := 8089.5; 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); Inc(Y, 30); QoLabel('Downlink begin (MHz)', Y);
FQoBegin := QoFloat(Y, 0.0, 50000.0, 0.001, 3); FQoBegin := QoFloat(Y, 0.0, 50000.0, 0.001, 3);
FQoBegin.Value := 10489.5; FQoBegin.Value := 10489.5;
@@ -3902,7 +4455,7 @@ begin
FQoInfo := TLabel.Create(Self); FQoInfo := TLabel.Create(Self);
FQoInfo.Parent := FPageQO; 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.Color := CLR_TEXTDIM; FQoInfo.Font.Name := 'Courier New';
FQoInfo.Font.Size := 9; FQoInfo.WordWrap := True; FQoInfo.Font.Size := 9; FQoInfo.WordWrap := True;
finally finally
@@ -3916,7 +4469,7 @@ begin
if FQoInfo = nil then Exit; if FQoInfo = nil then Exit;
dlMid := (FQoBegin.Value + FQoEnd.Value) / 2.0; // МГц dlMid := (FQoBegin.Value + FQoEnd.Value) / 2.0; // МГц
rxIf := dlMid - FQoLnbLO.Value; // RX IF в SDR 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( FQoInfo.Caption := Format(
'At downlink %.3f MHz: RX IF = %.3f MHz TX = %.3f MHz', 'At downlink %.3f MHz: RX IF = %.3f MHz TX = %.3f MHz',
[dlMid, rxIf, txF]); [dlMid, rxIf, txF]);
@@ -3931,6 +4484,7 @@ begin
FXvtrConfig.Entries[QO100_SLOT].LOOffset := FQoLnbLO.Value * 1.0e6; FXvtrConfig.Entries[QO100_SLOT].LOOffset := FQoLnbLO.Value * 1.0e6;
FXvtrConfig.Entries[QO100_SLOT].LOError := FQoLoErr.Value; FXvtrConfig.Entries[QO100_SLOT].LOError := FQoLoErr.Value;
FXvtrConfig.Entries[QO100_SLOT].TxLOOffset := FQoTxLO.Value * 1.0e6; 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].FreqBegin := FQoBegin.Value * 1.0e6;
FXvtrConfig.Entries[QO100_SLOT].FreqEnd := FQoEnd.Value * 1.0e6; FXvtrConfig.Entries[QO100_SLOT].FreqEnd := FQoEnd.Value * 1.0e6;
FXvtrConfig.Entries[QO100_SLOT].RXGain := FQoRXGain.Value; FXvtrConfig.Entries[QO100_SLOT].RXGain := FQoRXGain.Value;
@@ -3946,6 +4500,7 @@ begin
FQoLnbLO.Value := E.LOOffset / 1.0e6; FQoLnbLO.Value := E.LOOffset / 1.0e6;
FQoLoErr.Value := E.LOError; FQoLoErr.Value := E.LOError;
FQoTxLO.Value := E.TxLOOffset / 1.0e6; FQoTxLO.Value := E.TxLOOffset / 1.0e6;
FQoTxLOErr.Value := E.TxLOError;
FQoBegin.Value := E.FreqBegin / 1.0e6; FQoBegin.Value := E.FreqBegin / 1.0e6;
FQoEnd.Value := E.FreqEnd / 1.0e6; FQoEnd.Value := E.FreqEnd / 1.0e6;
FQoRXGain.Value := E.RXGain; FQoRXGain.Value := E.RXGain;
+31 -2
View File
@@ -191,6 +191,11 @@ type
// SliceId — логический id (из RadioController), не WDSP-канал. // SliceId — логический id (из RadioController), не WDSP-канал.
TOnSliceAudio = procedure(SliceId: Integer; const Left, Right: array of Single; TOnSliceAudio = procedure(SliceId: Integer; const Left, Right: array of Single;
Count: Integer) of object; Count: Integer) of object;
// Калибровочный офсет спектра/водопада (дБ) для дисплея: PanId 0 = главный,
// 1..MAX_PANS-1 = доп. панадаптер. Спрашивается раз в кадр из display-потока:
// величина зависит от диапазона/трансвертера, о которых знает только
// TRadioController (аналог SMeterCalFor для стрелки).
TOnDispCalQuery = function(PanId: Integer): Double of object;
// Пиксели спектра/водопада доп. пана — из display-потока (как FOnSpectrum/ // Пиксели спектра/водопада доп. пана — из display-потока (как FOnSpectrum/
// FOnWaterfall главного). Внутри колбэка НЕ звать slice/pan API движка. // FOnWaterfall главного). Внутри колбэка НЕ звать slice/pan API движка.
TOnPanPixels = procedure(PanId: Integer; const Pixels: array of Single; TOnPanPixels = procedure(PanId: Integer; const Pixels: array of Single;
@@ -331,6 +336,7 @@ type
FOnDemodAudio:TOnAudioReady; // pre-volume/mute tap for digital decoders FOnDemodAudio:TOnAudioReady; // pre-volume/mute tap for digital decoders
FOnSpectrum: TOnSpectrumReady; FOnSpectrum: TOnSpectrumReady;
FOnWaterfall: TOnWaterfallReady; FOnWaterfall: TOnWaterfallReady;
FOnDispCal: TOnDispCalQuery;
FOnTXIQ: TOnTXIQReady; FOnTXIQ: TOnTXIQReady;
// --- Мультислайсы --- // --- Мультислайсы ---
@@ -756,6 +762,8 @@ type
property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio; property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio;
property OnSliceDemodAudio: TOnSliceAudio property OnSliceDemodAudio: TOnSliceAudio
read FOnSliceDemodAudio write FOnSliceDemodAudio; read FOnSliceDemodAudio write FOnSliceDemodAudio;
// Офсет дисплея (дБ) на кадр — см. TOnDispCalQuery. nil = без коррекции.
property OnDispCal: TOnDispCalQuery read FOnDispCal write FOnDispCal;
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum; property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall; property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
// Пиксели доп. панов (display-поток). UI вешает при появлении панов (3.3). // Пиксели доп. панов (display-поток). UI вешает при появлении панов (3.3).
@@ -3810,18 +3818,24 @@ var
Flag: Integer; Flag: Integer;
i, N, p: Integer; i, N, p: Integer;
DispID: Integer; DispID: Integer;
Cal: Single;
GotSpec, GotWf: Boolean; GotSpec, GotWf: Boolean;
begin begin
if not FInitialized or not FAnalyzerOpen then Exit; if not FInitialized or not FAnalyzerOpen then Exit;
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS); N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
DispID := FActiveDisplayID; // RX_DISP_ID или TX_DISP_ID, зависит от FTXActive DispID := FActiveDisplayID; // RX_DISP_ID или TX_DISP_ID, зависит от FTXActive
// Калибровка дисплея (дБ) — сдвиг всей трассы, спрашиваем раз в кадр.
// На TX-дисплее коррекция входного тракта не при чём.
if Assigned(FOnDispCal) and (DispID = RX_DISP_ID) then Cal := FOnDispCal(0)
else Cal := 0;
Flag := 0; Flag := 0;
GetPixels(DispID, 0, @PixBuf[0], @Flag); GetPixels(DispID, 0, @PixBuf[0], @Flag);
if Flag <> 0 then if Flag <> 0 then
begin begin
for i := 0 to N - 1 do for i := 0 to N - 1 do
FSpectrumPixels[i] := PixBuf[i]; FSpectrumPixels[i] := PixBuf[i] + Cal;
if Assigned(FOnSpectrum) then if Assigned(FOnSpectrum) then
FOnSpectrum(FSpectrumPixels, N); FOnSpectrum(FSpectrumPixels, N);
end; end;
@@ -3831,7 +3845,7 @@ begin
if Flag <> 0 then if Flag <> 0 then
begin begin
for i := 0 to N - 1 do for i := 0 to N - 1 do
FWaterfallPixels[i] := PixBuf[i]; FWaterfallPixels[i] := PixBuf[i] + Cal;
if Assigned(FOnWaterfall) then if Assigned(FOnWaterfall) then
FOnWaterfall(FWaterfallPixels, N); FOnWaterfall(FWaterfallPixels, N);
end; end;
@@ -3858,6 +3872,18 @@ begin
finally finally
FSliceLock.Leave; FSliceLock.Leave;
end; end;
// Пан может стоять на своём диапазоне — офсет спрашиваем по его PanId.
if GotSpec or GotWf then
begin
if Assigned(FOnDispCal) then Cal := FOnDispCal(p) else Cal := 0;
if Cal <> 0 then
begin
if GotSpec then
for i := 0 to N - 1 do PixBuf[i] := PixBuf[i] + Cal;
if GotWf then
for i := 0 to N - 1 do WfBuf[i] := WfBuf[i] + Cal;
end;
end;
if GotSpec and Assigned(FOnPanSpectrum) then if GotSpec and Assigned(FOnPanSpectrum) then
FOnPanSpectrum(p, PixBuf, N); FOnPanSpectrum(p, PixBuf, N);
if GotWf and Assigned(FOnPanWaterfall) then if GotWf and Assigned(FOnPanWaterfall) then
@@ -3880,6 +3906,9 @@ function TWDSPEngine.GetSMeterDBm: Double;
begin begin
// НЕ вызываем GetRXAMeter здесь повторно — значение уже обновлено // НЕ вызываем GetRXAMeter здесь повторно — значение уже обновлено
// в DSP-колбэке. Повторный вызов сбрасывает аккумулятор WDSP → 0. // в DSP-колбэке. Повторный вызов сбрасывает аккумулятор WDSP → 0.
// Калибровочный офсет здесь НЕ прибавляется: он зависит от диапазона/
// трансвертера, о которых движок не знает — его добавляет TRadioController
// (ReadSMeterDBm / SliceSMeter).
Result := FSMeter; Result := FSMeter;
end; end;
+4 -4
View File
@@ -199,7 +199,7 @@ begin
'<option value="10">10 Hz</option><option value="100" selected>100 Hz</option>' + '<option value="10">10 Hz</option><option value="100" selected>100 Hz</option>' +
'<option value="1000">1 kHz</option><option value="10000">10 kHz</option>' + '<option value="1000">1 kHz</option><option value="10000">10 kHz</option>' +
'</select>' + '</select>' +
'<select id="attSel"><option value="0" selected>0 dB</option><option value="1">-10 dB</option><option value="2">-20 dB</option></select>' + '<div class="sl-g"><span class="sl-lb">ATT</span><input type="range" id="attSl" min="0" max="31" value="0"><span class="sl-val" id="attVal">0</span></div>' +
'<button id="nbBtn">NB</button>' + '<button id="nbBtn">NB</button>' +
'<button id="snbBtn">SNB</button>' + '<button id="snbBtn">SNB</button>' +
'<button id="anfBtn">ANF</button>' + '<button id="anfBtn">ANF</button>' +
@@ -311,7 +311,7 @@ begin
'q("modGrpSel").onchange=function(){const grp=GRP_MAP[modGrp()];const defaultMode=grp[0];cur.mode=defaultMode;syncModGrp(defaultMode);pend("mode");ws2({cmd:"mode",mode:defaultMode});};' + 'q("modGrpSel").onchange=function(){const grp=GRP_MAP[modGrp()];const defaultMode=grp[0];cur.mode=defaultMode;syncModGrp(defaultMode);pend("mode");ws2({cmd:"mode",mode:defaultMode});};' +
'q("modTglBtn").onclick=function(){audioKick();const next=modTglTarget();cur.mode=next;syncModGrp(next);pend("mode");ws2({cmd:"mode",mode:next});};' + 'q("modTglBtn").onclick=function(){audioKick();const next=modTglTarget();cur.mode=next;syncModGrp(next);pend("mode");ws2({cmd:"mode",mode:next});};' +
'q("agcSel").onchange=function(e){pend("agc");ws2({cmd:"agc",mode:+e.target.value});};' + 'q("agcSel").onchange=function(e){pend("agc");ws2({cmd:"agc",mode:+e.target.value});};' +
'q("attSel").onchange=function(e){pend("attn");ws2({cmd:"attn",idx:+e.target.value});};' + 'q("attSl").oninput=function(e){var v=+e.target.value;var lbl=q("attVal");if(lbl)lbl.textContent=v;pend("attn");ws2({cmd:"attn",db:v});};' +
// FNP — фильтровый оверлей // FNP — фильтровый оверлей
'var FNP_N={ssb:["5.0k","4.4k","3.8k","3.3k","2.9k","2.7k","2.4k","2.1k","1.8k","1.0k"],' + 'var FNP_N={ssb:["5.0k","4.4k","3.8k","3.3k","2.9k","2.7k","2.4k","2.1k","1.8k","1.0k"],' +
@@ -732,7 +732,7 @@ begin
'function isPending(field){return !!(pendingCmds[field]&&pendingCmds[field]>Date.now());}' + 'function isPending(field){return !!(pendingCmds[field]&&pendingCmds[field]>Date.now());}' +
// Маппинг: server JSON field → pend key // Маппинг: server JSON field → pend key
'var PEND_MAP={vfo_a_hz:"freq",vfo_b_hz:"freq",nr:"nr",nr_mode:"nr",nb:"nb",nb_mode:"nb",snb:"snb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc",transmitting:"mox",drive:"drive",attn_idx:"attn",tuning:"tun",filter_bw:"filter",mute:"mute",beacon_on:"beacon",rx_mute_on:"rxmute",rx_gain_mode:"rxgain",rx_gain_db:"rxgain"};' + 'var PEND_MAP={vfo_a_hz:"freq",vfo_b_hz:"freq",nr:"nr",nr_mode:"nr",nb:"nb",nb_mode:"nb",snb:"snb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc",transmitting:"mox",drive:"drive",attn_db:"attn",tuning:"tun",filter_bw:"filter",mute:"mute",beacon_on:"beacon",rx_mute_on:"rxmute",rx_gain_mode:"rxgain",rx_gain_db:"rxgain"};' +
'function applyState(msg){' + 'function applyState(msg){' +
'Object.keys(msg).forEach(function(k){var pk=PEND_MAP[k];if(!pk||!isPending(pk))cur[k]=msg[k];});' + 'Object.keys(msg).forEach(function(k){var pk=PEND_MAP[k];if(!pk||!isPending(pk))cur[k]=msg[k];});' +
@@ -786,7 +786,7 @@ begin
'if(!isPending("mute"))updMuteIc();' + 'if(!isPending("mute"))updMuteIc();' +
'if(!isPending("drive")){const ts=q("txSl");if(ts&&!ts.matches(":active")){ts.value=cur.drive||0;var tv=q("txVal");if(tv)tv.textContent=cur.drive||0;}}' + 'if(!isPending("drive")){const ts=q("txSl");if(ts&&!ts.matches(":active")){ts.value=cur.drive||0;var tv=q("txVal");if(tv)tv.textContent=cur.drive||0;}}' +
'if(!isPending("mode")){syncModGrp(cur.mode||0);syncStepSel();}' + 'if(!isPending("mode")){syncModGrp(cur.mode||0);syncStepSel();}' +
'if(!isPending("attn")){const at=q("attSel");if(at)at.value=cur.attn_idx||0;}' + 'if(!isPending("attn")){const at=q("attSl");if(at&&!at.matches(":active")){at.value=cur.attn_db||0;var av=q("attVal");if(av)av.textContent=cur.attn_db||0;}}' +
'updBand(activeVfo==="A"?(cur.vfo_a_hz||0):(vfoB||0));updSpan(cur.span_hz||192000);' + 'updBand(activeVfo==="A"?(cur.vfo_a_hz||0):(vfoB||0));updSpan(cur.span_hz||192000);' +
'paintSM(Number(cur.smeter_dbm||-130));' + 'paintSM(Number(cur.smeter_dbm||-130));' +
'updStatusBar();' + 'updStatusBar();' +
+9 -9
View File
@@ -296,7 +296,7 @@ type
FDriveLevel: Integer; FDriveLevel: Integer;
FOnFreqA: TWebCmdFreqA; FOnFreqA: TWebCmdFreqA;
FOnAttn: TWebCmdAttn; FOnAttn: TWebCmdAttn;
FAttnIdx: Integer; FAttnDB: Integer; // шаговый аттенюатор ADC, дБ (0..31)
FOnTun: TWebCmdTun; FOnTun: TWebCmdTun;
FOnFMStep: TWebCmdFMStep; FOnFMStep: TWebCmdFMStep;
FTuning: Boolean; FTuning: Boolean;
@@ -406,7 +406,7 @@ type
CenterHz: Double; FilterIdx: Integer; CenterHz: Double; FilterIdx: Integer;
VfoB: Double; ActiveVfo: Integer; VfoB: Double; ActiveVfo: Integer;
Transmitting: Boolean; DriveLevel: Integer; Transmitting: Boolean; DriveLevel: Integer;
AttnIdx: Integer; Tuning: Boolean; Duplex: Boolean; AttnDB: Integer; Tuning: Boolean; Duplex: Boolean;
FwdW, SWRV, PAMaxPower: Double; FwdW, SWRV, PAMaxPower: Double;
const StatusText, BoardText, IPText, SupplyText, PLLText, const StatusText, BoardText, IPText, SupplyText, PLLText,
RXText, TXText, SeqText: string); RXText, TXText, SeqText: string);
@@ -1572,10 +1572,10 @@ begin
end end
else if Cmd = 'attn' then else if Cmd = 'attn' then
begin begin
Idx := JsonGetInt(Json, 'idx', FAttnIdx); Idx := JsonGetInt(Json, 'db', FAttnDB);
if Idx < 0 then Idx := 0; if Idx < 0 then Idx := 0;
if Idx > 2 then Idx := 2; if Idx > 31 then Idx := 31;
FStateLock.Enter; FAttnIdx := Idx; FStateLock.Leave; FStateLock.Enter; FAttnDB := Idx; FStateLock.Leave;
if Assigned(FOnAttn) then FOnAttn(Idx); if Assigned(FOnAttn) then FOnAttn(Idx);
end end
else if Cmd = 'fm_step' then else if Cmd = 'fm_step' then
@@ -1778,7 +1778,7 @@ begin
'"wf_agc":%s,"wf_nf":%s,"band_idx":%d,"smeter_dbm":%.1f,' + '"wf_agc":%s,"wf_nf":%s,"band_idx":%d,"smeter_dbm":%.1f,' +
'"running":%s,"mute":%s,"ctun":%s,' + '"running":%s,"mute":%s,"ctun":%s,' +
'"nr_mode":%d,"nr":%s,"nb_mode":%d,"nb":%s,"snb":%s,"anf":%s,"connected":%s,' + '"nr_mode":%d,"nr":%s,"nb_mode":%d,"nb":%s,"snb":%s,"anf":%s,"connected":%s,' +
'"transmitting":%s,"drive":%d,"attn_idx":%d,"tuning":%s,"dup":%s,' + '"transmitting":%s,"drive":%d,"attn_db":%d,"tuning":%s,"dup":%s,' +
'"fwd_w":%.1f,"swr":%.2f,"pa_max_power":%.0f,' + '"fwd_w":%.1f,"swr":%.2f,"pa_max_power":%.0f,' +
'"status_text":"%s","board_text":"%s","ip_text":"%s","supply_text":"%s",' + '"status_text":"%s","board_text":"%s","ip_text":"%s","supply_text":"%s",' +
'"pll_text":"%s","rx_text":"%s","tx_text":"%s","seq_text":"%s",' + '"pll_text":"%s","rx_text":"%s","tx_text":"%s","seq_text":"%s",' +
@@ -1809,7 +1809,7 @@ begin
BoolToStr(FConnected, 'true', 'false'), BoolToStr(FConnected, 'true', 'false'),
BoolToStr(FTransmitting, 'true', 'false'), BoolToStr(FTransmitting, 'true', 'false'),
FDriveLevel, FDriveLevel,
FAttnIdx, FAttnDB,
BoolToStr(FTuning, 'true', 'false'), BoolToStr(FTuning, 'true', 'false'),
BoolToStr(FDuplex, 'true', 'false'), BoolToStr(FDuplex, 'true', 'false'),
FFwdW, FSWR, FPAMaxPower, FFwdW, FSWR, FPAMaxPower,
@@ -1889,7 +1889,7 @@ procedure TWebServer.PushSpectrum(
CenterHz: Double; FilterIdx: Integer; CenterHz: Double; FilterIdx: Integer;
VfoB: Double; ActiveVfo: Integer; VfoB: Double; ActiveVfo: Integer;
Transmitting: Boolean; DriveLevel: Integer; Transmitting: Boolean; DriveLevel: Integer;
AttnIdx: Integer; Tuning: Boolean; Duplex: Boolean; AttnDB: Integer; Tuning: Boolean; Duplex: Boolean;
FwdW, SWRV, PAMaxPower: Double; FwdW, SWRV, PAMaxPower: Double;
const StatusText, BoardText, IPText, SupplyText, PLLText, const StatusText, BoardText, IPText, SupplyText, PLLText,
RXText, TXText, SeqText: string); RXText, TXText, SeqText: string);
@@ -1926,7 +1926,7 @@ begin
FActiveVfo := ActiveVfo; FActiveVfo := ActiveVfo;
FTransmitting := Transmitting; FTransmitting := Transmitting;
FDriveLevel := DriveLevel; FDriveLevel := DriveLevel;
FAttnIdx := AttnIdx; FAttnDB := AttnDB;
FTuning := Tuning; FTuning := Tuning;
FDuplex := Duplex; FDuplex := Duplex;
FFwdW := FwdW; FFwdW := FwdW;
+4 -2
View File
@@ -17,9 +17,9 @@
<UseVersionInfo Value="True"/> <UseVersionInfo Value="True"/>
<AutoIncrementBuild Value="True"/> <AutoIncrementBuild Value="True"/>
<MinorVersionNr Value="9"/> <MinorVersionNr Value="9"/>
<BuildNr Value="276"/> <BuildNr Value="277"/>
</VersionInfo> </VersionInfo>
<MacroValues Count="106"> <MacroValues Count="108">
<Macro1 Name="LCLWidgetType" Value="qt6"/> <Macro1 Name="LCLWidgetType" Value="qt6"/>
<Macro2 Name="LCLWidgetType" Value="qt6"/> <Macro2 Name="LCLWidgetType" Value="qt6"/>
<Macro3 Name="LCLWidgetType" Value="qt6"/> <Macro3 Name="LCLWidgetType" Value="qt6"/>
@@ -126,6 +126,8 @@
<Macro104 Name="LCLWidgetType" Value="qt6"/> <Macro104 Name="LCLWidgetType" Value="qt6"/>
<Macro105 Name="LCLWidgetType" Value="qt6"/> <Macro105 Name="LCLWidgetType" Value="qt6"/>
<Macro106 Name="LCLWidgetType" Value="qt6"/> <Macro106 Name="LCLWidgetType" Value="qt6"/>
<Macro107 Name="LCLWidgetType" Value="qt6"/>
<Macro108 Name="LCLWidgetType" Value="qt6"/>
</MacroValues> </MacroValues>
<BuildModes> <BuildModes>
<Item Name="Debug" Default="True"/> <Item Name="Debug" Default="True"/>
+1 -1
View File
@@ -466,7 +466,7 @@ begin
// не обновляет в headless — освежаем здесь, как GUI-таймер (MainForm). Без // не обновляет в headless — освежаем здесь, как GUI-таймер (MainForm). Без
// этого PushState слал бы клиентам залипший -130 dBm. // этого PushState слал бы клиентам залипший -130 dBm.
if FCtrl.FRunning and FCtrl.FWDSPReady then if FCtrl.FRunning and FCtrl.FWDSPReady then
FCtrl.FLastSMeter := FCtrl.FDSPEngine.GetSMeterDBm; FCtrl.FLastSMeter := FCtrl.ReadSMeterDBm;
FWebAd.PushState; // исходящее зеркало состояния/спектра FWebAd.PushState; // исходящее зеркало состояния/спектра
lastPush := nowMs; lastPush := nowMs;
end; end;