mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
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:
+24
-4
@@ -156,6 +156,9 @@ type
|
||||
FMainDither: Boolean;
|
||||
FMainRandom: Boolean;
|
||||
|
||||
// Частотная калибровка опорника радио (ppm) — вносится в частотное слово
|
||||
// (тактовый генератор радио менять нечем, поэтому правим запрос).
|
||||
FFreqCalPPM: Double;
|
||||
// Текущее состояние для построения HP пакетов
|
||||
FCurrentRXFreq: Double;
|
||||
FCurrentTXFreq: Double;
|
||||
@@ -260,6 +263,9 @@ type
|
||||
procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte;
|
||||
Transmitting, PAEnabled, AlexEnabled: Boolean); override;
|
||||
procedure SetStepAtten(dB: Byte); override;
|
||||
procedure SetFreqCalPPM(P: Double); override;
|
||||
// Частота с поправкой опорника — для частотного слова DDC/DUC.
|
||||
function CalFreq(Hz: Double): Double;
|
||||
procedure SetAlexConfig(const A: TAlexSettings); override;
|
||||
// Включение/настройка XVTR-режима. Все параметры применяются мгновенно
|
||||
// (отправляется HP-пакет). Когда AEnable=False, XVTR-bit гасится,
|
||||
@@ -736,6 +742,7 @@ begin
|
||||
FWBUpdateRateMS := 70;
|
||||
FWBFrameCount := 0;
|
||||
FWBSeqValid := False;
|
||||
FFreqCalPPM := 0.0;
|
||||
FCurrentRXFreq := 7100000;
|
||||
FCurrentTXFreq := 7100000;
|
||||
FCurrentDrive := 0;
|
||||
@@ -1693,6 +1700,19 @@ begin
|
||||
FAlexEnabled := AlexEnabled;
|
||||
end;
|
||||
|
||||
function THPSDRNetwork.CalFreq(Hz: Double): Double;
|
||||
// Опора радио выше номинала на FFreqCalPPM ⇒ DDC/DUC уходят вверх на столько
|
||||
// же. Компенсируем запрошенной частотой: просим ниже ровно на ppm.
|
||||
begin
|
||||
if FFreqCalPPM = 0 then Exit(Hz);
|
||||
Result := Hz / (1.0 + FFreqCalPPM * 1.0E-6);
|
||||
end;
|
||||
|
||||
procedure THPSDRNetwork.SetFreqCalPPM(P: Double);
|
||||
begin
|
||||
FFreqCalPPM := P;
|
||||
end;
|
||||
|
||||
procedure THPSDRNetwork.SetStepAtten(dB: Byte);
|
||||
begin
|
||||
if dB > 31 then dB := 31;
|
||||
@@ -1737,7 +1757,7 @@ begin
|
||||
DDCBase := DDCBaseIndex;
|
||||
|
||||
// DDC RX frequency (bytes 9 + DDC*4)
|
||||
Ph := FreqToPhaseWord(FCurrentRXFreq);
|
||||
Ph := FreqToPhaseWord(CalFreq(FCurrentRXFreq));
|
||||
Buf[9 + DDCBase*4] := (Ph shr 24) and $FF;
|
||||
Buf[10 + DDCBase*4] := (Ph shr 16) and $FF;
|
||||
Buf[11 + DDCBase*4] := (Ph shr 8) and $FF;
|
||||
@@ -1748,7 +1768,7 @@ begin
|
||||
for ddc := 0 to MAX_DDCS - 1 do
|
||||
if FPanDDCEnabled[ddc] then
|
||||
begin
|
||||
Ph := FreqToPhaseWord(FPanDDCFreqHz[ddc]);
|
||||
Ph := FreqToPhaseWord(CalFreq(FPanDDCFreqHz[ddc]));
|
||||
Buf[9 + ddc*4] := (Ph shr 24) and $FF;
|
||||
Buf[10 + ddc*4] := (Ph shr 16) and $FF;
|
||||
Buf[11 + ddc*4] := (Ph shr 8) and $FF;
|
||||
@@ -1759,7 +1779,7 @@ begin
|
||||
// DDCBase=2 ⇒ с частотным словом главного DDC (bytes 17..20) не пересекаемся.
|
||||
if FPSTXActive then
|
||||
begin
|
||||
Ph := FreqToPhaseWord(FCurrentTXFreq);
|
||||
Ph := FreqToPhaseWord(CalFreq(FCurrentTXFreq));
|
||||
Buf[9] := (Ph shr 24) and $FF;
|
||||
Buf[10] := (Ph shr 16) and $FF;
|
||||
Buf[11] := (Ph shr 8) and $FF;
|
||||
@@ -1771,7 +1791,7 @@ begin
|
||||
end;
|
||||
|
||||
// DUC TX frequency (bytes 329-332)
|
||||
Ph := FreqToPhaseWord(FCurrentTXFreq);
|
||||
Ph := FreqToPhaseWord(CalFreq(FCurrentTXFreq));
|
||||
Buf[329] := (Ph shr 24) and $FF;
|
||||
Buf[330] := (Ph shr 16) and $FF;
|
||||
Buf[331] := (Ph shr 8) and $FF;
|
||||
|
||||
+65
-6
@@ -400,7 +400,9 @@ function BoardSupportsSupplyCurrent(BoardType: Integer): Boolean;
|
||||
|
||||
// Convert raw ADC value to supply current in Amps (Board 5 / Orion MkII only)
|
||||
// Uses UserADC1 (bytes 55-56). Returns -1 for unsupported boards.
|
||||
function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer): Double;
|
||||
// VoffMV/SensMVA — калибровка датчика (Thetis AmpVoff/AmpSens, ACS712-style).
|
||||
function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer;
|
||||
VoffMV: Double = 360.0; SensMVA: Double = 120.0): Double;
|
||||
|
||||
// Convert raw ADC value to RF power in Watts (ANAN-100)
|
||||
function ADCToWatts100(RawADC: Word): Double;
|
||||
@@ -408,6 +410,16 @@ function ADCToWatts100(RawADC: Word): Double;
|
||||
// Convert raw ADC value to RF power in Watts (ANAN-10)
|
||||
function ADCToWatts10(RawADC: Word): Double;
|
||||
|
||||
// Alex bridge FWD/REV detector → Watts, константы Thetis computeAlexFwdPower/
|
||||
// computeRefPower по модели трансивера:
|
||||
// 0 = ANAN-10/100/100B/100D (Vref 3.3V)
|
||||
// 1 = ANAN-200D / Orion (Vref 5.0V)
|
||||
// 2 = ANAN-7000DLE / Anvelina Pro 3 / G2 / Red Pitaya
|
||||
// 3 = ANAN-8000DLE / Orion MkII
|
||||
// IsRev — REV-детектор (свой bridge_volt/offset), Is6m — 6m-коррекция REV.
|
||||
function AlexDetToWatts(RawADC: Word; DetModel: Integer;
|
||||
IsRev, Is6m: Boolean): Double;
|
||||
|
||||
implementation
|
||||
|
||||
procedure PackU32BE(Value: LongWord; out B: array of Byte; Offset: Integer);
|
||||
@@ -476,14 +488,15 @@ begin
|
||||
Result := BoardType = 5;
|
||||
end;
|
||||
|
||||
function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer): Double;
|
||||
function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer;
|
||||
VoffMV: Double; SensMVA: Double): Double;
|
||||
begin
|
||||
// Board 5 (Orion MkII): UserADC1, Vref=5V → mV = adc/4095*5000.
|
||||
// Current sensor: Voff=360mV, Sens=120mV/A (Thetis defaults, ACS712-style).
|
||||
// Formula: amps = (mV - Voff) / Sens.
|
||||
if BoardType = 5 then
|
||||
// Formula: amps = (mV - Voff) / Sens. Дефолты Voff=360мВ, Sens=120мВ/А
|
||||
// (Thetis, ACS712-style); переопределяются калибровкой.
|
||||
if (BoardType = 5) and (SensMVA > 0) then
|
||||
begin
|
||||
Result := ((RawADC / 4095.0) * 5000.0 - 360.0) / 120.0;
|
||||
Result := ((RawADC / 4095.0) * 5000.0 - VoffMV) / SensMVA;
|
||||
if Result < 0.0 then Result := 0.0;
|
||||
end else
|
||||
Result := -1.0;
|
||||
@@ -507,4 +520,50 @@ begin
|
||||
Result := (V * V) / 0.09;
|
||||
end;
|
||||
|
||||
function AlexDetToWatts(RawADC: Word; DetModel: Integer;
|
||||
IsRev, Is6m: Boolean): Double;
|
||||
var
|
||||
Bridge, Vref, V: Double;
|
||||
CalOff: Integer;
|
||||
begin
|
||||
// Константы из Thetis (console.cs computeAlexFwdPower / computeRefPower).
|
||||
// W = ((ADC - CalOff)/4095 * Vref)^2 / Bridge.
|
||||
case DetModel of
|
||||
1: begin // ANAN-200D / Orion
|
||||
Vref := 5.0;
|
||||
if IsRev then begin
|
||||
Bridge := 0.108; if Is6m then Bridge := 0.5;
|
||||
CalOff := 2;
|
||||
end else begin
|
||||
Bridge := 0.108; CalOff := 4;
|
||||
end;
|
||||
end;
|
||||
2: begin // ANAN-7000DLE / Anvelina Pro 3 / G2 / Red Pitaya
|
||||
Vref := 5.0;
|
||||
if IsRev then begin
|
||||
Bridge := 0.15; if Is6m then Bridge := 0.7;
|
||||
CalOff := 28;
|
||||
end else begin
|
||||
Bridge := 0.12; CalOff := 32;
|
||||
end;
|
||||
end;
|
||||
3: begin // ANAN-8000DLE / Orion MkII
|
||||
Vref := 5.0; Bridge := 0.08;
|
||||
if IsRev then CalOff := 16 else CalOff := 18;
|
||||
end;
|
||||
else begin // 0: ANAN-10/100/100B/100D
|
||||
Vref := 3.3;
|
||||
if IsRev then begin
|
||||
Bridge := 0.095; if Is6m then Bridge := 0.5;
|
||||
CalOff := 3;
|
||||
end else begin
|
||||
Bridge := 0.095; CalOff := 6;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
V := ((RawADC - CalOff) / 4095.0) * Vref;
|
||||
if V < 0 then V := 0;
|
||||
Result := (V * V) / Bridge;
|
||||
end;
|
||||
|
||||
end.
|
||||
|
||||
@@ -72,6 +72,14 @@ var
|
||||
iio_channel_attr_write_double: function(chn: Piio_channel; attr: PAnsiChar;
|
||||
val: cdouble): cint; cdecl;
|
||||
|
||||
// ---- Атрибуты устройства (не канальные) ----
|
||||
// ad9361-phy: 'xo_correction' — фактическая частота опорного XO в Гц, какой её
|
||||
// считает драйвер. Правка = частотная калибровка всего чипа (RX и TX LO).
|
||||
iio_device_attr_read_longlong: function(dev: Piio_device; attr: PAnsiChar;
|
||||
out val: clonglong): cint; cdecl;
|
||||
iio_device_attr_write_longlong: function(dev: Piio_device; attr: PAnsiChar;
|
||||
val: clonglong): cint; cdecl;
|
||||
|
||||
// ---- Debug-атрибуты устройства (debugfs) ----
|
||||
// Нужны для выбора антенного разъёма у AD936x в режиме 1R1T:
|
||||
// 'adi,1rx-1tx-mode-use-rx-num'/'-tx-num' (какой физический канал кормит тракт)
|
||||
@@ -185,6 +193,8 @@ begin
|
||||
iio_channel_attr_write := Sym('iio_channel_attr_write');
|
||||
iio_channel_attr_write_bool := Sym('iio_channel_attr_write_bool');
|
||||
iio_channel_attr_write_double := Sym('iio_channel_attr_write_double');
|
||||
iio_device_attr_read_longlong := Sym('iio_device_attr_read_longlong');
|
||||
iio_device_attr_write_longlong := Sym('iio_device_attr_write_longlong');
|
||||
iio_device_debug_attr_read := Sym('iio_device_debug_attr_read');
|
||||
iio_device_debug_attr_write := Sym('iio_device_debug_attr_write');
|
||||
iio_device_debug_attr_write_longlong := Sym('iio_device_debug_attr_write_longlong');
|
||||
|
||||
+101
-7
@@ -347,6 +347,11 @@ type
|
||||
FPSPopup: TObject; // TPureSignalPopup (cast при использовании)
|
||||
|
||||
PanelRXBlock: TPanel;
|
||||
// Шаговый аттенюатор ADC (openHPSDR): плавные 0..31 дБ. На Pluto ряд
|
||||
// скрыт — там усиление тракта живёт в блоке RF AGC.
|
||||
LblAtt: TLabel;
|
||||
LblAttVal: TLabel;
|
||||
TrkAtt: TFlatSlider;
|
||||
BtnChannels: TFlatButton;
|
||||
FChannelsDropDown: TFlatDropDown;
|
||||
FChannelsForm: TObject; // TChannelsForm (cast при использовании)
|
||||
@@ -698,6 +703,7 @@ type
|
||||
function DefaultMicSource: TTXMicSource;
|
||||
procedure ApplyMOX(Active: Boolean);
|
||||
procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double);
|
||||
procedure OnCalibrationSettingsChange(const C: TCalibration);
|
||||
procedure OnPlutoTxMaxAttChange(MaxAttDb: Double);
|
||||
procedure OnAnt936xChange(const A: TAD936xAnt); // разъёмы AD936x (Antenna)
|
||||
procedure OnVHFCalSettingsChange(const VHFCal: array of Double);
|
||||
@@ -705,6 +711,8 @@ type
|
||||
procedure BtnAGCModeClick(Sender: TObject);
|
||||
procedure TrkAGCChange(Sender: TObject);
|
||||
procedure TrkRxGainChange(Sender: TObject);
|
||||
procedure TrkAttChange(Sender: TObject);
|
||||
procedure UpdateAttLabel;
|
||||
// (BtnPlutoGainClick объявлен выше)
|
||||
procedure MeterTimerTick(Sender: TObject);
|
||||
procedure SpectrumTimerTick(Sender: TObject);
|
||||
@@ -761,6 +769,13 @@ uses SettingsForm;
|
||||
|
||||
{$R *.lfm}
|
||||
|
||||
const
|
||||
// Высота RX-блока левой панели: с рядом ATT (openHPSDR) и без него (Pluto —
|
||||
// там усиление тракта в блоке RF AGC). Переключает LayoutLeftPanel, он же
|
||||
// двигает панели ниже.
|
||||
RX_BLOCK_H = 96;
|
||||
RX_BLOCK_H_ATT = 120;
|
||||
|
||||
function TMainForm.LeftPanelButtonWidth(PanelWidth, ColCount,
|
||||
ColIndex: Integer): Integer;
|
||||
var
|
||||
@@ -1504,6 +1519,12 @@ begin
|
||||
TrkVolume.Position := FController.ActiveVolume; // RX или TX-монитор
|
||||
rfRxGain:
|
||||
UpdateRxGainUI;
|
||||
rfAtten:
|
||||
if TrkAtt <> nil then
|
||||
begin
|
||||
TrkAtt.Position := FController.FAtten;
|
||||
UpdateAttLabel;
|
||||
end;
|
||||
rfDrive:
|
||||
begin
|
||||
TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive%
|
||||
@@ -2027,7 +2048,7 @@ begin
|
||||
// RX block: CTUN + VOL + NR/NB/SNB/ANF/MUTE
|
||||
PanelRXBlock := TPanel.Create(Self);
|
||||
PanelRXBlock.Parent := PanelLeft;
|
||||
PanelRXBlock.SetBounds(0, Y, LEFT_W, 96);
|
||||
PanelRXBlock.SetBounds(0, Y, LEFT_W, RX_BLOCK_H_ATT);
|
||||
PanelRXBlock.BevelOuter := bvNone;
|
||||
MakeLbl(PanelRXBlock, 'RX', 4, 2);
|
||||
|
||||
@@ -2083,7 +2104,29 @@ begin
|
||||
LeftPanelButtonLeft(LEFT_W, 5, 4), 70,
|
||||
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
|
||||
PanelAGC := TPanel.Create(Self);
|
||||
@@ -3408,6 +3451,7 @@ procedure TMainForm.SpectrumTimerTick(Sender: TObject);
|
||||
var
|
||||
NowTick: QWord;
|
||||
ElapsedTick: QWord;
|
||||
LastPktTick: QWord;
|
||||
I: Integer;
|
||||
begin
|
||||
// «×» шапки пана: закрытие отложено сюда (нельзя free панель из её кнопки).
|
||||
@@ -3437,8 +3481,12 @@ begin
|
||||
begin
|
||||
NowTick := GetTickCount64;
|
||||
ElapsedTick := NowTick - FController.FRXStartTime;
|
||||
// Сетевой поток может обновить FRXLastPktTime на 1 ms ПОСЛЕ NowTick.
|
||||
// QWord-вычитание в таком случае underflow'ится в огромное значение и
|
||||
// давало ложный "Connection lost" при совершенно живом DDC-потоке.
|
||||
LastPktTick := FController.FRXLastPktTime;
|
||||
// Ждём первый пакет 5 секунд после старта
|
||||
if (FController.FRXLastPktTime = 0) and (ElapsedTick > 5000) then
|
||||
if (LastPktTick = 0) and (ElapsedTick > 5000) then
|
||||
begin
|
||||
BtnStartStop.Caption := 'START';
|
||||
StyleButton(BtnStartStop, False);
|
||||
@@ -3450,7 +3498,8 @@ begin
|
||||
Exit;
|
||||
end;
|
||||
// После первого пакета — следим чтобы поток не прерывался более 3 сек
|
||||
if (FController.FRXLastPktTime > 0) and ((NowTick - FController.FRXLastPktTime) > 3000) then
|
||||
if (LastPktTick > 0) and (NowTick >= LastPktTick) and
|
||||
((NowTick - LastPktTick) > 3000) then
|
||||
begin
|
||||
BtnStartStop.Caption := 'START';
|
||||
StyleButton(BtnStartStop, False);
|
||||
@@ -3474,7 +3523,7 @@ begin
|
||||
|
||||
if FController.FWDSPReady then
|
||||
begin
|
||||
FController.FLastSMeter := FController.FDSPEngine.GetSMeterDBm;
|
||||
FController.FLastSMeter := FController.ReadSMeterDBm;
|
||||
FController.FDSPEngine.SetSpectrumWidth(FPan.SpectrumWidth);
|
||||
Inc(FAgcLineCounter);
|
||||
if FAgcLineCounter >= 6 then
|
||||
@@ -7413,6 +7462,12 @@ begin
|
||||
UpdateDriveLabel; // изменился потолок → пересчитать дБ-подпись drive
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnCalibrationSettingsChange(const C: TCalibration);
|
||||
begin
|
||||
// Логика (санитизация/офсет S-метра в DSP/persist) — в контроллере.
|
||||
FController.SetCalibration(C);
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnAnt936xChange(const A: TAD936xAnt);
|
||||
// Вкладка Antenna (AD936x): таблица «диапазон → разъём». Контроллер сохраняет её
|
||||
// и сразу применяет разъёмы текущего диапазона/трансвертера.
|
||||
@@ -7920,6 +7975,19 @@ begin
|
||||
FController.SetRxGain(TrkRxGain.Position); // переводит в manual
|
||||
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);
|
||||
begin
|
||||
if FSyncingFromController then Exit;
|
||||
@@ -7936,10 +8004,20 @@ begin
|
||||
for i := 0 to 3 do
|
||||
if BtnPlutoGain[i] <> nil then
|
||||
StyleButton(BtnPlutoGain[i], BtnPlutoGain[i].Tag = FController.FRxGainMode);
|
||||
TrkRxGain.Position := FController.FRxGainDb;
|
||||
TrkRxGain.Enabled := (FController.FRxGainMode = 0); // активен только в manual
|
||||
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
|
||||
LblRxGain.Caption := 'AGC';
|
||||
finally
|
||||
@@ -7953,6 +8031,17 @@ procedure TMainForm.LayoutLeftPanel(Pluto: Boolean);
|
||||
var baseTop: Integer;
|
||||
begin
|
||||
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;
|
||||
PanelPlutoAGC.Top := baseTop;
|
||||
PanelPlutoAGC.Visible := Pluto;
|
||||
@@ -8226,6 +8315,7 @@ begin
|
||||
SF.OnVisibilityChange := ApplyVisibility;
|
||||
SF.OnFPSChange := ApplyFPS;
|
||||
SF.OnPAChange := OnPASettingsChange;
|
||||
SF.OnCalibrationChange := OnCalibrationSettingsChange;
|
||||
SF.OnPlutoTxChange := OnPlutoTxMaxAttChange;
|
||||
SF.OnAnt936xChange := OnAnt936xChange;
|
||||
SF.OnVHFCalChange := OnVHFCalSettingsChange;
|
||||
@@ -8248,9 +8338,13 @@ begin
|
||||
// физических разъёмов на диапазон/трансвертер (если разъёмов две пары).
|
||||
SF.SetAntennaBackend(FController.BackendCaps.Kind = bkPluto,
|
||||
FController.BackendCaps.HasAntSwitch);
|
||||
// Вкладка Calibration: band-план и состав групп — по активному железу
|
||||
// (у AD936x нет моста детектора мощности и телеметрии питания).
|
||||
SF.SetCalibrationBackend(FController.BackendCaps.Kind = bkPluto);
|
||||
SF.LoadAnt936x(FController.Ant936x, FController.FXvtrSettings);
|
||||
SF.LoadXvtrSettings(FController.FXvtrSettings);
|
||||
SF.LoadPASettings(FController.FPAMaxPower, FController.FPABandCal);
|
||||
SF.LoadCalibration(FController.FCal);
|
||||
SF.LoadPlutoTxMaxAtt(FController.FPlutoTxMaxAttDb);
|
||||
SF.LoadVHFCalSettings(FController.FVHFBandCal);
|
||||
SF.LoadCATSettings(
|
||||
|
||||
+74
-4
@@ -46,6 +46,8 @@ type
|
||||
FTXFreq: Double;
|
||||
FSampleRate: Integer;
|
||||
FTransmitting: Boolean;
|
||||
FFreqCalPPM: Double; // частотная калибровка опорника, ppm (xo_correction)
|
||||
FXoNomHz: Int64; // номинал XO, Гц (снимается при коннекте, 0 = не знаем)
|
||||
FGainMode: string; // gain_control_mode (manual/slow_attack/...)
|
||||
FGainDb: Integer; // hardwaregain при ручном режиме
|
||||
|
||||
@@ -109,13 +111,16 @@ type
|
||||
FTeleTemp: Double; // TELEMETRY_NONE, пока не прочитано
|
||||
FTeleSoc: Double;
|
||||
FTeleRssi: Double;
|
||||
FTeleGain: Double; // hardwaregain RX, дБ (в AGC-режимах крутит железо)
|
||||
FTeleValid: Boolean; // был ли хоть один успешный опрос
|
||||
procedure StartTelemetry;
|
||||
procedure StopTelemetry;
|
||||
// Блокирующий опрос железа: только из FTeleThread.
|
||||
function PollTelemetryBlocking(out TempC, SocTempC, RSSIdB: Double): Boolean;
|
||||
function PollTelemetryBlocking(out TempC, SocTempC, RSSIdB,
|
||||
GainDb: Double): Boolean;
|
||||
|
||||
// Низкоуровневые помощники применения атрибутов к ad9361-phy
|
||||
procedure ApplyFreqCal;
|
||||
procedure ApplyRxLO(Hz: Double);
|
||||
procedure ApplyTxLO(Hz: Double);
|
||||
procedure SetTxLOPowerdown(PoweredOn: Boolean); // TX LO вкл/выкл (по PTT)
|
||||
@@ -174,6 +179,8 @@ type
|
||||
procedure SendDUCIQ(const IData, QData: array of Integer); override;
|
||||
procedure ClearDUCIQQueue; override;
|
||||
function ReadTelemetry(out TempC, SocTempC, RSSIdB: Double): Boolean; override;
|
||||
function RxGainNowDb: Double; override;
|
||||
procedure SetFreqCalPPM(P: Double); override;
|
||||
end;
|
||||
|
||||
// Стабильный 6-байтовый ключ (synthetic MAC) из serial/URI — для per-device
|
||||
@@ -407,12 +414,14 @@ begin
|
||||
FSampleRate := 1536000;
|
||||
FGainMode := 'slow_attack'; // аппаратный AGC по умолчанию
|
||||
FGainDb := 40;
|
||||
FFreqCalPPM := 0.0; FXoNomHz := 0;
|
||||
FRxBuf := nil; FRxChI := nil; FRxChQ := nil; FRxThread := nil;
|
||||
FStreaming := False; FRxSeq := 0;
|
||||
FTxBuf := nil; FTxChI := nil; FTxChQ := nil; FTxThread := nil;
|
||||
FTxStreaming := False; FTxRatio := 8;
|
||||
FTeleThread := nil; FTeleValid := False;
|
||||
FTeleTemp := TELEMETRY_NONE; FTeleSoc := TELEMETRY_NONE; FTeleRssi := TELEMETRY_NONE;
|
||||
FTeleGain := TELEMETRY_NONE;
|
||||
InitCriticalSection(FTeleLock);
|
||||
FTxAttDb := PLUTO_TX_OFF; // на старте TX выключен (макс. аттенюация)
|
||||
// FIFO на ~4 TX-буфера запаса под джиттер продюсера/консьюмера. Окончательный
|
||||
@@ -631,6 +640,7 @@ begin
|
||||
ApplyTxMux;
|
||||
ApplyGain(True);
|
||||
ApplyTxAtten; // TX заглушён (макс. аттенюация) до явного PTT
|
||||
ApplyFreqCal; // частотная калибровка опорника (xo_correction)
|
||||
StartTelemetry;
|
||||
Result := True;
|
||||
end;
|
||||
@@ -652,6 +662,40 @@ end;
|
||||
|
||||
// ---- Применение атрибутов к ad9361-phy ----
|
||||
|
||||
procedure TPlutoBackend.ApplyFreqCal;
|
||||
// xo_correction — частота опорного XO в Гц, какой её считает драйвер. Правка
|
||||
// разъезжается на ВСЁ: RX LO, TX LO, sample rate — один параметр вместо
|
||||
// подгонки offset'ов по трансвертерам.
|
||||
// Номинал снимаем округлением прочитанного до 100 кГц: в железе могла
|
||||
// остаться наша же коррекция с прошлой сессии (она сбрасывается только по
|
||||
// перезагрузке платы), а ±100 ppm от 40 МГц — это ±4 кГц, округление их
|
||||
// съедает и компаундинга не будет.
|
||||
var
|
||||
V: clonglong;
|
||||
begin
|
||||
if (FPhy = nil) or (not IIOLoaded)
|
||||
or (not Assigned(iio_device_attr_read_longlong))
|
||||
or (not Assigned(iio_device_attr_write_longlong)) then Exit;
|
||||
if FXoNomHz = 0 then
|
||||
begin
|
||||
if iio_device_attr_read_longlong(FPhy, 'xo_correction', V) <> 0 then Exit;
|
||||
if V <= 0 then Exit;
|
||||
FXoNomHz := (Int64(V) + 50000) div 100000 * 100000;
|
||||
end;
|
||||
iio_device_attr_write_longlong(FPhy, 'xo_correction',
|
||||
Round(FXoNomHz * (1.0 + FFreqCalPPM * 1.0E-6)));
|
||||
// Синтезаторы считают делители в момент записи частоты — переписываем LO,
|
||||
// иначе правка подхватится только на следующей перестройке.
|
||||
if FRXFreq > 0 then ApplyRxLO(FRXFreq);
|
||||
if FTXFreq > 0 then ApplyTxLO(FTXFreq);
|
||||
end;
|
||||
|
||||
procedure TPlutoBackend.SetFreqCalPPM(P: Double);
|
||||
begin
|
||||
FFreqCalPPM := P;
|
||||
ApplyFreqCal;
|
||||
end;
|
||||
|
||||
procedure TPlutoBackend.ApplyRxLO(Hz: Double);
|
||||
var Ch: Piio_channel;
|
||||
begin
|
||||
@@ -1133,7 +1177,8 @@ begin
|
||||
LeaveCriticalSection(FTxLock);
|
||||
end;
|
||||
|
||||
function TPlutoBackend.PollTelemetryBlocking(out TempC, SocTempC, RSSIdB: Double): Boolean;
|
||||
function TPlutoBackend.PollTelemetryBlocking(out TempC, SocTempC, RSSIdB,
|
||||
GainDb: Double): Boolean;
|
||||
// БЛОКИРУЮЩИЙ опрос железа — только из FTeleThread (каждое чтение = USB round-trip):
|
||||
// ad9361-phy temp0/input — температура трансивера в миллиградусах C (longlong);
|
||||
// xadc temp0 — температура Zynq SoC/FPGA; сырой raw + offset (longlong)
|
||||
@@ -1149,6 +1194,7 @@ begin
|
||||
TempC := TELEMETRY_NONE;
|
||||
SocTempC := TELEMETRY_NONE;
|
||||
RSSIdB := TELEMETRY_NONE;
|
||||
GainDb := TELEMETRY_NONE;
|
||||
Result := False;
|
||||
if (not FConnected) or (FPhy = nil) or (not IIOLoaded) then Exit;
|
||||
|
||||
@@ -1190,6 +1236,29 @@ begin
|
||||
RSSIdB := d;
|
||||
Result := True;
|
||||
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;
|
||||
|
||||
@@ -1244,7 +1313,7 @@ const
|
||||
POLL_MS = 500; // ~2 Гц: чаще незачем, каждый опрос ~30 мс USB-транзакций
|
||||
STEP_MS = 25; // мелкий шаг сна — чтобы Terminate отрабатывал быстро
|
||||
var
|
||||
t, s, r: Double;
|
||||
t, s, r, g: Double;
|
||||
ok: Boolean;
|
||||
Waited: Integer;
|
||||
begin
|
||||
@@ -1252,7 +1321,7 @@ begin
|
||||
begin
|
||||
if FBk.FConnected then
|
||||
begin
|
||||
ok := FBk.PollTelemetryBlocking(t, s, r);
|
||||
ok := FBk.PollTelemetryBlocking(t, s, r, g);
|
||||
if ok then
|
||||
begin
|
||||
EnterCriticalSection(FBk.FTeleLock);
|
||||
@@ -1260,6 +1329,7 @@ begin
|
||||
FBk.FTeleTemp := t;
|
||||
FBk.FTeleSoc := s;
|
||||
FBk.FTeleRssi := r;
|
||||
FBk.FTeleGain := g;
|
||||
FBk.FTeleValid := True;
|
||||
finally
|
||||
LeaveCriticalSection(FBk.FTeleLock);
|
||||
|
||||
@@ -199,6 +199,14 @@ type
|
||||
// RSSI приёмника (дБ). HPSDR — нет такой (использует HP-status), возвращает False.
|
||||
// Недоступные значения = TELEMETRY_NONE.
|
||||
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;
|
||||
@@ -300,6 +308,15 @@ begin
|
||||
Result := False;
|
||||
end;
|
||||
|
||||
function TRadioBackend.RxGainNowDb: Double;
|
||||
begin
|
||||
Result := TELEMETRY_NONE;
|
||||
end;
|
||||
|
||||
procedure TRadioBackend.SetFreqCalPPM(P: Double);
|
||||
begin
|
||||
end;
|
||||
|
||||
procedure TRadioBackend.SetTxAtten(Db: Double);
|
||||
begin
|
||||
end;
|
||||
|
||||
+164
-14
@@ -375,6 +375,10 @@ type
|
||||
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings;
|
||||
FPABandCal: array[0..CFG_BAND_COUNT-1] of Double;
|
||||
FVHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double;
|
||||
// Калибровки (детектор мощности/КСВ, напряжение/ток, S-метр) — вкладка
|
||||
// Calibration. Читается сетевым потоком в OnHPStatus (double-поля, torn
|
||||
// read безопасен как у FPABandCal); менять через SetCalibration.
|
||||
FCal: TCalibration;
|
||||
FDevMAC: array[0..5] of Byte;
|
||||
// Глобальные настройки устройства, загруженные ConnectDevice. Хранятся для
|
||||
// UI-рендера полей, которых нет в контроллере (DisplayFPS/FreqMhzDigits/CAT).
|
||||
@@ -387,6 +391,9 @@ type
|
||||
// RSSI приёмника дБ. Опрашиваются ServicePlutoTelemetry с UI-таймера;
|
||||
// TELEMETRY_NONE = нет данных.
|
||||
FLastPlutoTempC, FLastPlutoSocTempC, FLastPlutoRSSIdB: Double;
|
||||
// Фактическое усиление RX-тракта AD936x (дБ) — в режимах AGC его крутит
|
||||
// железо. Снимается тем же опросом; UI показывает его в блоке RF AGC.
|
||||
FLastPlutoGainDb: Double;
|
||||
FPlutoTeleTick: QWord; // GetTickCount64 последнего опроса (троттлинг)
|
||||
FPlutoTempBad: Integer; // подряд отброшенных выбросов температуры (анти-залипание)
|
||||
|
||||
@@ -448,6 +455,10 @@ type
|
||||
procedure OnAudioReady(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);
|
||||
// Общая выборка калибровочной таблицы «диапазон/трансвертер» по частоте.
|
||||
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 OnWaterfallReady(const Pixels: array of Single; Count: Integer);
|
||||
procedure OnMicPacket(const Data: TMicDataPacket);
|
||||
@@ -593,7 +604,7 @@ type
|
||||
procedure ToggleMute;
|
||||
procedure SetDrive(V: Integer);
|
||||
procedure DriveBy(Delta: Integer);
|
||||
procedure SetAtten(Idx: Integer);
|
||||
procedure SetAtten(Db: Integer); // ADC step attenuator, дБ (0..31)
|
||||
|
||||
// ---- Мультислайсы (софт-fan-out) ----
|
||||
// Создаёт слайс на TargetHz: заводит WDSP-канал в движке + свой аудио-выход
|
||||
@@ -619,6 +630,17 @@ type
|
||||
procedure SetSliceDSP(Id, NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
||||
procedure SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer);
|
||||
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);
|
||||
function SliceCount: Integer;
|
||||
function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
|
||||
@@ -681,6 +703,7 @@ type
|
||||
function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent
|
||||
procedure ApplyPlutoTxAtten; // ставит att по FTransmitting/FTuning
|
||||
procedure SetPlutoTxMaxAtt(Db: Double); // потолок на drive=100% (внешний усилитель)
|
||||
procedure SetCalibration(const C: TCalibration); // вкладка Calibration → FCal (офсеты S-метра — в SMeterCalFor)
|
||||
procedure ApplyWDSPDrive(Pct: Integer); // цифровой drive WDSP: Pluto=1.0 (мощность — атт), HPSDR=Pct/100
|
||||
|
||||
// Диапазоны / трансвертеры / спан
|
||||
@@ -943,9 +966,11 @@ begin
|
||||
FFMStepOn := True;
|
||||
FRawFMDeviation := RAW_FM_DEV_DEFAULT;
|
||||
FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно
|
||||
TSettingsManager.DefaultCalibration(FCal);
|
||||
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
|
||||
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE;
|
||||
FLastPlutoRSSIdB := TELEMETRY_NONE;
|
||||
FLastPlutoGainDb := TELEMETRY_NONE;
|
||||
FPlutoTeleTick := 0; FPlutoTempBad := 0;
|
||||
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
|
||||
end;
|
||||
@@ -964,6 +989,9 @@ begin
|
||||
FDSPEngine.OnSliceAudio := OnSliceAudioReady;
|
||||
FDSPEngine.OnSliceDemodAudio := OnSliceDemodAudioReady;
|
||||
FDSPEngine.OnDemodAudio := OnDemodAudioReady;
|
||||
// Калибровка уровня спектра/водопада на диапазон/трансвертер — движок
|
||||
// спрашивает её раз в кадр (сам он про диапазоны не знает).
|
||||
FDSPEngine.OnDispCal := DispCalFor;
|
||||
FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||||
|
||||
// QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
|
||||
@@ -1186,10 +1214,12 @@ procedure TRadioController.OnHPStatus(const Status: THighPriorityStatus);
|
||||
const
|
||||
SWR_MIN_FWD_W = 1.0; // ниже — fwd это ADC-шум, SWR не считаем
|
||||
SWR_CAP = 9.9;
|
||||
BAND_6M = 10; // индекс 6m в HF-плане (160m..6m)
|
||||
var
|
||||
FwdPwr, RevPwr: Word;
|
||||
SupplyV, SupplyA, FwdW, SWRV, Rho: Double;
|
||||
IsTx: Boolean;
|
||||
SupplyV, SupplyA, FwdW, RevW, SWRV, Rho, DetK: Double;
|
||||
Bi: Integer;
|
||||
Is6m, IsTx: Boolean;
|
||||
begin
|
||||
FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo;
|
||||
RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo;
|
||||
@@ -1208,19 +1238,31 @@ begin
|
||||
SupplyV := ADCToSupplyVolts(
|
||||
(Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo,
|
||||
FNetwork.Device.BoardType);
|
||||
if SupplyV >= 0 then SupplyV := SupplyV * FCal.SupplyVoltCal;
|
||||
|
||||
if BoardSupportsSupplyCurrent(FNetwork.Device.BoardType) then
|
||||
SupplyA := ADCToSupplyCurrent(
|
||||
(Status.UserADC1Hi shl 8) or Status.UserADC1Lo,
|
||||
FNetwork.Device.BoardType);
|
||||
FNetwork.Device.BoardType,
|
||||
FCal.AmpVoffMV, FCal.AmpSensMVA);
|
||||
end;
|
||||
FwdW := ADCToWatts100(FwdPwr);
|
||||
// Детектор Alex-моста: константы по модели (вкладка Calibration) + per-band
|
||||
// множитель. Множитель общий для FWD/REV, поэтому КСВ от него не зависит
|
||||
// (как Thetis PABandCalMult).
|
||||
Bi := TxBandIdx;
|
||||
Is6m := Bi = BAND_6M;
|
||||
DetK := 1.0;
|
||||
if (Bi >= 0) and (Bi < CFG_BAND_COUNT) and (FCal.PADetCal[Bi] > 0) then
|
||||
DetK := FCal.PADetCal[Bi];
|
||||
FwdW := AlexDetToWatts(FwdPwr, FCal.PwrDetModel, False, Is6m) * DetK;
|
||||
RevW := AlexDetToWatts(RevPwr, FCal.PwrDetModel, True, Is6m) * DetK;
|
||||
// FTransmitting — программный флаг; HPS_PTT — вход HW-PTT (footswitch/mic).
|
||||
IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0);
|
||||
// SWR = (1+ρ)/(1-ρ), ρ = adc_r/adc_f; только при значимой fwd-мощности.
|
||||
if IsTx and (FwdW >= SWR_MIN_FWD_W) and (FwdPwr > 0) then
|
||||
// SWR = (1+ρ)/(1-ρ), ρ = sqrt(RevW/FwdW) (Thetis); только при значимой
|
||||
// fwd-мощности.
|
||||
if IsTx and (FwdW >= SWR_MIN_FWD_W) then
|
||||
begin
|
||||
Rho := RevPwr / FwdPwr;
|
||||
Rho := Sqrt(RevW / FwdW);
|
||||
if Rho >= 0.99 then
|
||||
SWRV := SWR_CAP
|
||||
else
|
||||
@@ -2018,9 +2060,78 @@ begin
|
||||
end;
|
||||
|
||||
function TRadioController.SliceSMeter(Id: Integer): Double;
|
||||
// Слайс может стоять на другом диапазоне (паны N кросс-банд), поэтому офсет
|
||||
// берём по ЕГО видимой частоте, а не по главному диапазону.
|
||||
var idx: Integer;
|
||||
begin
|
||||
if Assigned(FDSPEngine) then Result := FDSPEngine.GetSliceSMeter(Id)
|
||||
else Result := -130.0;
|
||||
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;
|
||||
|
||||
procedure TRadioController.SetSliceDevice(Id, DevIndex: Integer;
|
||||
@@ -2184,7 +2295,9 @@ begin
|
||||
// QO-100 / split-LO: TX идёт на собственный uplink-LO (visible - TxLOOffset),
|
||||
// без LOError — дрейф LNB к передаче отношения не имеет.
|
||||
if FXvtrSettings.Entries[FCurrentXvtr].FullDuplexTx then
|
||||
// TO Error: сам транспондер-offset держим стандартным, уход тракта — здесь.
|
||||
Result := VisibleHz - FXvtrSettings.Entries[FCurrentXvtr].TxLOOffset
|
||||
+ FXvtrSettings.Entries[FCurrentXvtr].TxLOError
|
||||
else
|
||||
// Обычный трансвертер: RX=TX LO (с калибровкой LOError) — как раньше.
|
||||
Result := VisibleHz
|
||||
@@ -2723,6 +2836,7 @@ begin
|
||||
// Аппаратный AGC AD936x — на диапазон (усиление тракта разное на 2м и QO-100).
|
||||
Result.RxGainMode := FRxGainMode;
|
||||
Result.RxGainDb := FRxGainDb;
|
||||
Result.AttenDB := FAtten; // шаговый аттенюатор ADC — тоже на диапазон
|
||||
// Waterfall AGC/NF — глобальные (не диапазонные)
|
||||
end;
|
||||
|
||||
@@ -2761,6 +2875,7 @@ begin
|
||||
FXvtrSettings.Entries[i].LastPanSlider := FPanSlider;
|
||||
FXvtrSettings.Entries[i].LastRxGainMode := FRxGainMode;
|
||||
FXvtrSettings.Entries[i].LastRxGainDb := FRxGainDb;
|
||||
FXvtrSettings.Entries[i].LastAttenDB := FAtten;
|
||||
end;
|
||||
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||||
Exit;
|
||||
@@ -2794,6 +2909,7 @@ begin
|
||||
Result.PlutoTxMaxAttDb := FPlutoTxMaxAttDb;
|
||||
for i := 0 to CFG_BAND_COUNT - 1 do Result.PABandCal[i] := FPABandCal[i];
|
||||
for i := 0 to CFG_XVTR_COUNT - 1 do Result.VHFBandCal[i] := FVHFBandCal[i];
|
||||
Result.Cal := FCal;
|
||||
Result.ActiveVfo := FActiveVfo;
|
||||
Result.NRMode := FNRMode;
|
||||
Result.NBMode := FNBMode;
|
||||
@@ -2889,6 +3005,11 @@ begin
|
||||
ApplyRxGainToBackend;
|
||||
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 ---
|
||||
FFMSQOn := B.FMSQOn;
|
||||
FFMSQLevel := B.FMSQLevel;
|
||||
@@ -3539,6 +3660,21 @@ begin
|
||||
SaveGlobalSettings; // персист потолка (per-device)
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetCalibration(const C: TCalibration);
|
||||
begin
|
||||
FCal := C;
|
||||
// Санитизация: битый конфиг не должен дать деление на ноль/абсурдные ватты.
|
||||
FCal.PwrDetModel := EnsureRange(FCal.PwrDetModel, 0, 3);
|
||||
if FCal.AmpSensMVA <= 0 then FCal.AmpSensMVA := 120.0;
|
||||
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);
|
||||
// На Pluto мощность задаётся ОДИН раз — железной аттенюацией (PlutoTxAttForDrive),
|
||||
// поэтому цифровой уровень WDSP держим на максимуме (1.0), а не режем по drive%.
|
||||
@@ -3549,12 +3685,20 @@ begin
|
||||
else FDSPEngine.SetDriveLevel(EnsureRange(Pct, 0, 100) / 100.0);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetAtten(Idx: Integer);
|
||||
procedure TRadioController.SetAtten(Db: Integer);
|
||||
// Шаговый аттенюатор ADC — плавный 0..31 дБ (как в железе openHPSDR), а не
|
||||
// три фиксированные ступени. Уровни S-метра/спектра компенсируются на ту же
|
||||
// величину (CalLookup), поэтому шкала dBm остаётся абсолютной.
|
||||
begin
|
||||
if Idx < 0 then Idx := 0 else if Idx > 2 then Idx := 2;
|
||||
FAtten := Idx;
|
||||
if Assigned(FNetwork) then FNetwork.SetStepAtten(FAtten * 10);
|
||||
FAtten := EnsureRange(Db, 0, 31);
|
||||
if Assigned(FNetwork) then FNetwork.SetStepAtten(Byte(FAtten));
|
||||
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;
|
||||
|
||||
procedure TRadioController.SetBand(Idx: Integer);
|
||||
@@ -3645,6 +3789,7 @@ begin
|
||||
B.FMRptOffsetHz := E.LastFMRptOffsetHz;
|
||||
B.RxGainMode := E.LastRxGainMode; // hw-AGC слота (AD936x)
|
||||
B.RxGainDb := E.LastRxGainDb;
|
||||
B.AttenDB := E.LastAttenDB; // аттенюатор ADC слота
|
||||
B.VfoA := Vis;
|
||||
B.VfoB := FVfoB;
|
||||
ApplyBandDSP(B);
|
||||
@@ -4921,6 +5066,7 @@ begin
|
||||
FActiveDDC := 0;
|
||||
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE;
|
||||
FLastPlutoRSSIdB := TELEMETRY_NONE;
|
||||
FLastPlutoGainDb := TELEMETRY_NONE;
|
||||
FPlutoTeleTick := 0; FPlutoTempBad := 0;
|
||||
Changed(rfRunning); // безусловный сброс run-кнопки/MOX.Enabled/sleep (как старый хвост STOP)
|
||||
end;
|
||||
@@ -5018,6 +5164,7 @@ begin
|
||||
FPABandCal[i] := FLoadedGlobal.PABandCal[i];
|
||||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||||
FVHFBandCal[i] := FLoadedGlobal.VHFBandCal[i];
|
||||
SetCalibration(FLoadedGlobal.Cal);
|
||||
// drive% (0..100) — раньше грузился через TrkDrive.Position; теперь напрямую,
|
||||
// слайдер выставит rfDevice-рендер. Калиброванный байт считаем после FCurrentBand.
|
||||
FDrivePercent := EnsureRange(FLoadedGlobal.DriveLevel, 0, 100);
|
||||
@@ -5378,6 +5525,9 @@ begin
|
||||
|
||||
// RSSI быстро меняется с сигналом — без сглаживания; обновляем, только если валиден.
|
||||
if r > TELEMETRY_NONE then FLastPlutoRSSIdB := r;
|
||||
// Фактическое усиление тракта (в MANUAL = заданное, в AGC — снятое с железа).
|
||||
FLastPlutoGainDb := FNetwork.RxGainNowDb;
|
||||
Changed(rfRxGain); // UI: живая цифра усиления в блоке RF AGC
|
||||
|
||||
Changed(rfSupply); // поле телеметрии статус-бара (поле 3); web-адаптер тоже
|
||||
end;
|
||||
|
||||
+103
-1
@@ -103,6 +103,9 @@ type
|
||||
// сильно разное на 2м и на QO-100, поэтому режим и manual-гейн per-band.
|
||||
RxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid
|
||||
RxGainDb: Integer; // manual gain, дБ (0..73)
|
||||
// Шаговый аттенюатор ADC (openHPSDR) — на диапазон: на 40м ночью нужен,
|
||||
// на 10м нет. 0..31 дБ.
|
||||
AttenDB: Integer;
|
||||
end;
|
||||
|
||||
// Персист панадаптеров и слайсов (секция "pans" под MAC). Восстанавливается
|
||||
@@ -267,6 +270,10 @@ type
|
||||
// собственный у SDR). FullDuplexTx помечает слот как такой транспондер.
|
||||
FullDuplexTx: Boolean; // True — раздельный TX-LO (QO-100); авто display-duplex
|
||||
TxLOOffset: Double; // Hz, TX LO-offset (для QO-100 = транспондер-offset, ~8089.5 МГц)
|
||||
// Hz, поправка к TX LO-offset (TO Error). Сам offset держим стандартным
|
||||
// (8089.5 МГц у QO-100), а уход конкретного тракта правим здесь.
|
||||
// Знак как у LOError: положительная — TX IF растёт.
|
||||
TxLOError: Double;
|
||||
RXGain: Double; // dB, компенсация усиления RX тракта
|
||||
RXOnly: Boolean; // True — запрещает TX на этом трансверторе
|
||||
TXPower: Integer; // 0..100 % (drive level специально для XVTR)
|
||||
@@ -295,6 +302,7 @@ type
|
||||
// Аппаратный AGC AD936x на слот трансвертера (у QO-100 своё усиление тракта).
|
||||
LastRxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid
|
||||
LastRxGainDb: Integer; // manual gain, дБ
|
||||
LastAttenDB: Integer; // шаговый аттенюатор ADC слота, дБ (0..31)
|
||||
end;
|
||||
|
||||
TXvtrSettings = record
|
||||
@@ -330,6 +338,34 @@ type
|
||||
EnableXvtrHf: Boolean; // режим XVTR HF
|
||||
end;
|
||||
|
||||
// Калибровки (вкладка Calibration): детектор мощности/КСВ Alex-моста,
|
||||
// напряжение/ток питания, S-метр. Модель детектора задаёт константы моста
|
||||
// (Vref/bridge_volt/ADC-offset, как в Thetis computeAlexFwdPower).
|
||||
TCalibration = record
|
||||
PwrDetModel: Integer; // 0=ANAN-10/100 (3.3V), 1=ANAN-200D, 2=ANAN-7000DLE/Anvelina Pro 3, 3=ANAN-8000DLE/Orion MkII
|
||||
PADetCal: array[0..CFG_BAND_COUNT-1] of Double; // множитель FWD/REV на банд, 0.50..2.00 (1.0 = без коррекции)
|
||||
SupplyVoltCal: Double; // множитель напряжения питания (1.0 = без коррекции)
|
||||
AmpVoffMV: Double; // сдвиг нуля датчика тока, мВ (Thetis default 360)
|
||||
AmpSensMVA: Double; // чувствительность датчика тока, мВ/А (Thetis default 120)
|
||||
// Офсет 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
|
||||
Volume: Integer;
|
||||
TxMonVolume: Integer; // громкость self-monitor RX-аудио на TX (RX MUTE off)
|
||||
@@ -397,6 +433,8 @@ type
|
||||
PlutoTxMaxAttDb: Double; // Pluto: TX-аттенюация на drive=100%, дБ<0 (дефолт -10)
|
||||
PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0
|
||||
VHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double; // калибровка для XVTR слотов 38.8..100.0
|
||||
// --- Calibration (детектор мощности/КСВ, напряжение/ток, S-метр) ---
|
||||
Cal: TCalibration;
|
||||
// --- ADC settings ---
|
||||
DitherEnabled: Boolean; // DDC Specific byte 5 — уменьшает интермодуляцию
|
||||
RandomEnabled: Boolean; // DDC Specific byte 6 — рандомизация ADC
|
||||
@@ -454,6 +492,7 @@ type
|
||||
class function MacToStr(const MAC: array of Byte): string;
|
||||
class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings);
|
||||
class procedure DefaultGlobal(out G: TGlobalSettings);
|
||||
class procedure DefaultCalibration(out C: TCalibration);
|
||||
class procedure DefaultTX(out T: TTXSettings);
|
||||
// Размер окна — не привязан к MAC, хранится в корне JSON
|
||||
procedure SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean);
|
||||
@@ -533,6 +572,7 @@ begin
|
||||
B.SpanHz := 192000;
|
||||
B.RxGainMode := 2; // slow_attack (как глобальный дефолт Pluto)
|
||||
B.RxGainDb := 40;
|
||||
B.AttenDB := 0;
|
||||
B.FMSQOn := False;
|
||||
B.FMSQLevel := 30;
|
||||
B.CTCSSOn := False;
|
||||
@@ -614,6 +654,19 @@ begin
|
||||
// ADC — включены по умолчанию: поддерживаются на Hermes, Angelia, Orion
|
||||
G.DitherEnabled := True;
|
||||
G.RandomEnabled := True;
|
||||
DefaultCalibration(G.Cal);
|
||||
end;
|
||||
|
||||
class procedure TSettingsManager.DefaultCalibration(out C: TCalibration);
|
||||
var i: Integer;
|
||||
begin
|
||||
FillChar(C, SizeOf(C), 0);
|
||||
C.PwrDetModel := 0; // ANAN-10/100 — совпадает с прежней зашитой формулой
|
||||
for i := 0 to CFG_BAND_COUNT-1 do C.PADetCal[i] := 1.0;
|
||||
C.SupplyVoltCal := 1.0;
|
||||
C.AmpVoffMV := 360.0;
|
||||
C.AmpSensMVA := 120.0;
|
||||
// SMeterBandDB/SMeterXvtrDB — нули после FillChar (без коррекции).
|
||||
end;
|
||||
|
||||
constructor TSettingsManager.Create(const FilePath: string);
|
||||
@@ -741,7 +794,7 @@ end;
|
||||
function TSettingsManager.LoadDevice(const MAC: array of Byte;
|
||||
out G: TGlobalSettings;
|
||||
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
|
||||
MacStr := MacToStr(MAC);
|
||||
Result := FRoot.Find(MacStr) <> nil;
|
||||
@@ -856,6 +909,30 @@ begin
|
||||
// ADC settings
|
||||
G.DitherEnabled := JB(GObj,'dither_enabled', True);
|
||||
G.RandomEnabled := JB(GObj,'random_enabled', True);
|
||||
// Calibration (детектор мощности/КСВ, напряжение/ток, S-метр)
|
||||
G.Cal.PwrDetModel := EnsureRange(JI(GObj,'cal_pwr_det_model', 0), 0, 3);
|
||||
for i := 0 to CFG_BAND_COUNT-1 do
|
||||
G.Cal.PADetCal[i] := EnsureRange(JD(GObj,'cal_pa_det_'+IntToStr(i), 1.0), 0.5, 2.0);
|
||||
G.Cal.SupplyVoltCal := EnsureRange(JD(GObj,'cal_supply_volt', 1.0), 0.1, 5.0);
|
||||
G.Cal.AmpVoffMV := EnsureRange(JD(GObj,'cal_amp_voff_mv', 360.0), 0.0, 2000.0);
|
||||
G.Cal.AmpSensMVA := EnsureRange(JD(GObj,'cal_amp_sens_mva', 120.0), 1.0, 1000.0);
|
||||
// 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
|
||||
begin
|
||||
@@ -883,6 +960,7 @@ begin
|
||||
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].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;
|
||||
|
||||
@@ -965,6 +1043,22 @@ begin
|
||||
// ADC settings
|
||||
JW(O,'dither_enabled',G.DitherEnabled);
|
||||
JW(O,'random_enabled',G.RandomEnabled);
|
||||
// Calibration
|
||||
JW(O,'cal_pwr_det_model',G.Cal.PwrDetModel);
|
||||
for i := 0 to CFG_BAND_COUNT-1 do
|
||||
JW(O,'cal_pa_det_'+IntToStr(i),G.Cal.PADetCal[i]);
|
||||
JW(O,'cal_supply_volt',G.Cal.SupplyVoltCal);
|
||||
JW(O,'cal_amp_voff_mv',G.Cal.AmpVoffMV);
|
||||
JW(O,'cal_amp_sens_mva',G.Cal.AmpSensMVA);
|
||||
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;
|
||||
|
||||
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,'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,'atten_db',B.AttenDB);
|
||||
end;
|
||||
|
||||
function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer;
|
||||
@@ -1015,6 +1110,7 @@ begin
|
||||
B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz);
|
||||
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.AttenDB := EnsureRange(JI(O,'atten_db', B.AttenDB), 0, 31);
|
||||
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);
|
||||
end;
|
||||
@@ -1514,6 +1610,7 @@ begin
|
||||
X.Entries[i].LOError := 0.0;
|
||||
X.Entries[i].FullDuplexTx := False;
|
||||
X.Entries[i].TxLOOffset := 0.0;
|
||||
X.Entries[i].TxLOError := 0.0;
|
||||
X.Entries[i].RXGain := 0.0;
|
||||
X.Entries[i].RXOnly := False;
|
||||
X.Entries[i].TXPower := 100;
|
||||
@@ -1541,6 +1638,7 @@ begin
|
||||
X.Entries[i].LastPanSlider := 0.5;
|
||||
X.Entries[i].LastRxGainMode := 2; // slow_attack
|
||||
X.Entries[i].LastRxGainDb := 40;
|
||||
X.Entries[i].LastAttenDB := 0;
|
||||
end;
|
||||
// Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились)
|
||||
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].FullDuplexTx := JB(EObj, 'full_dup', X.Entries[i].FullDuplexTx);
|
||||
X.Entries[i].TxLOOffset := JD(EObj, 'tx_lo_off', X.Entries[i].TxLOOffset);
|
||||
X.Entries[i].TxLOError := JD(EObj, 'tx_lo_error', X.Entries[i].TxLOError);
|
||||
X.Entries[i].RXGain := JD(EObj, 'rx_gain', X.Entries[i].RXGain);
|
||||
X.Entries[i].RXOnly := JB(EObj, 'rx_only', X.Entries[i].RXOnly);
|
||||
X.Entries[i].TXPower := EnsureRange(JI(EObj, 'tx_pwr', X.Entries[i].TXPower), 0, 100);
|
||||
@@ -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].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].LastAttenDB := EnsureRange(JI(EObj, 'last_atten_db', X.Entries[i].LastAttenDB), 0, 31);
|
||||
// Валидация: Last freqs должны быть в [FreqBegin..FreqEnd]
|
||||
if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or
|
||||
(X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then
|
||||
@@ -1695,6 +1795,7 @@ begin
|
||||
JW(EObj, 'lo_err', X.Entries[i].LOError);
|
||||
JW(EObj, 'full_dup', X.Entries[i].FullDuplexTx);
|
||||
JW(EObj, 'tx_lo_off', X.Entries[i].TxLOOffset);
|
||||
JW(EObj, 'tx_lo_error', X.Entries[i].TxLOError);
|
||||
JW(EObj, 'rx_gain', X.Entries[i].RXGain);
|
||||
JW(EObj, 'rx_only', X.Entries[i].RXOnly);
|
||||
JW(EObj, 'tx_pwr', X.Entries[i].TXPower);
|
||||
@@ -1722,6 +1823,7 @@ begin
|
||||
JW(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider);
|
||||
JW(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode);
|
||||
JW(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb);
|
||||
JW(EObj, 'last_atten_db', X.Entries[i].LastAttenDB);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
+563
-8
@@ -41,6 +41,8 @@ type
|
||||
TOnAnt936xChange = procedure(const A: TAD936xAnt) of object;
|
||||
// VHF (XVTR) PA calibration: 8 slots, 38.8..100.0
|
||||
TOnVHFCalChange = procedure(const VHFCal: array of Double) of object;
|
||||
// Калибровки (вкладка Calibration): детектор мощности/КСВ, напряжение/ток, S-метр
|
||||
TOnCalibrationChange = procedure(const C: TCalibration) of object;
|
||||
// TX-настройки целиком (любая правка → один callback с актуальной структурой)
|
||||
TOnTXSettingsChange = procedure(const T: TTXSettings) of object;
|
||||
|
||||
@@ -150,6 +152,45 @@ type
|
||||
FEdVHFCal: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit;
|
||||
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 ----
|
||||
// Текущее состояние, отдаётся в OnTXChange целиком при любой правке.
|
||||
FTX: TTXSettings;
|
||||
@@ -277,6 +318,7 @@ type
|
||||
FQoLnbLO: TFlatFloatSpinEdit; // МГц — LNB LO (= LOOffset, RX)
|
||||
FQoLoErr: TFlatFloatSpinEdit; // Гц — LO Error (= LOError, живая калибровка LNB / beacon-lock)
|
||||
FQoTxLO: TFlatFloatSpinEdit; // МГц — транспондер-offset (= TxLOOffset)
|
||||
FQoTxLOErr: TFlatFloatSpinEdit; // Гц — поправка offset'а (TO Error)
|
||||
FQoBegin: TFlatFloatSpinEdit; // МГц — downlink begin
|
||||
FQoEnd: TFlatFloatSpinEdit; // МГц — downlink end
|
||||
FQoRXGain: TFlatFloatSpinEdit; // dB
|
||||
@@ -337,6 +379,19 @@ type
|
||||
procedure BuildDisplayTab;
|
||||
procedure BuildRX1Tab;
|
||||
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 BuildTransmitTab;
|
||||
procedure BuildAlexTab;
|
||||
@@ -445,6 +500,9 @@ type
|
||||
procedure LoadFreqMhzDigits(Digits: Integer);
|
||||
procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double);
|
||||
procedure LoadPlutoTxMaxAtt(MaxAttDb: Double);
|
||||
procedure LoadCalibration(const C: TCalibration);
|
||||
// Железо вкладки Calibration: band-план и состав групп (Pluto/openHPSDR).
|
||||
procedure SetCalibrationBackend(IsPluto: Boolean);
|
||||
// Вкладка Antenna: Alex-таблица (openHPSDR) либо таблица разъёмов AD936x.
|
||||
procedure SetAntennaBackend(IsAD936x, HasAntSwitch: Boolean);
|
||||
// Таблица разъёмов + имена/видимость слотов трансвертеров.
|
||||
@@ -474,6 +532,7 @@ type
|
||||
property OnPlutoTxChange: TOnPlutoTxChange read FOnPlutoTxChange write FOnPlutoTxChange;
|
||||
property OnAnt936xChange: TOnAnt936xChange read FOnAnt936xChange write FOnAnt936xChange;
|
||||
property OnVHFCalChange: TOnVHFCalChange read FOnVHFCalChange write FOnVHFCalChange;
|
||||
property OnCalibrationChange: TOnCalibrationChange read FOnCalibrationChange write FOnCalibrationChange;
|
||||
property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange;
|
||||
property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange;
|
||||
property OnWfRenderChange: TOnWfRenderChange read FOnWfRenderChange write FOnWfRenderChange;
|
||||
@@ -550,6 +609,21 @@ const
|
||||
PA_BAND_NAMES: array[0..10] of string =
|
||||
('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 }
|
||||
|
||||
constructor TSettingsForm.Create(AOwner: TComponent);
|
||||
@@ -704,11 +778,12 @@ begin
|
||||
FNavWaterfall := MakeNavButton('Waterfall', 182);
|
||||
FNavTransmit := MakeNavButton('Transmit', 218);
|
||||
FNavPA := MakeNavButton('Power Amplifier', 254);
|
||||
FNavAdvanced := MakeNavButton('Advanced', 290);
|
||||
FNavCAT := MakeNavButton('CAT', 326);
|
||||
FNavAlex := MakeNavButton('Antenna / Alex', 362);
|
||||
FNavXvtr := MakeNavButton('Transverter', 398);
|
||||
FNavQO := MakeNavButton('QO-100', 434);
|
||||
FNavCalib := MakeNavButton('Calibration', 290);
|
||||
FNavAdvanced := MakeNavButton('Advanced', 326);
|
||||
FNavCAT := MakeNavButton('CAT', 362);
|
||||
FNavAlex := MakeNavButton('Antenna / Alex', 398);
|
||||
FNavXvtr := MakeNavButton('Transverter', 434);
|
||||
FNavQO := MakeNavButton('QO-100', 470);
|
||||
|
||||
FContentPanel := TPanel.Create(Self);
|
||||
FContentPanel.Parent := Self;
|
||||
@@ -722,6 +797,7 @@ begin
|
||||
FPageWaterfall := MakeScrollPage;
|
||||
FPageTransmit := MakeScrollPage;
|
||||
FPagePA := MakeScrollPage;
|
||||
FPageCalib := MakeScrollPage;
|
||||
FPageAdvanced := MakeScrollPage;
|
||||
FPageCAT := MakeScrollPage;
|
||||
FPageAlex := MakeScrollPage;
|
||||
@@ -730,12 +806,14 @@ begin
|
||||
FAlexBoardType := 0;
|
||||
TSettingsManager.DefaultAlex(FAlexConfig);
|
||||
TSettingsManager.DefaultXvtr(FXvtrConfig);
|
||||
TSettingsManager.DefaultCalibration(FCalConfig);
|
||||
|
||||
BuildAudioTab;
|
||||
BuildDisplayTab;
|
||||
BuildRX1Tab;
|
||||
BuildTransmitTab;
|
||||
BuildPATab;
|
||||
BuildCalibrationTab;
|
||||
BuildAdvancedTab;
|
||||
BuildCATTab;
|
||||
BuildAlexTab;
|
||||
@@ -768,6 +846,7 @@ begin
|
||||
else if Sender = FNavWaterfall then SelectPage(FPageWaterfall, FNavWaterfall)
|
||||
else if Sender = FNavTransmit then SelectPage(FPageTransmit, FNavTransmit)
|
||||
else if Sender = FNavPA then SelectPage(FPagePA, FNavPA)
|
||||
else if Sender = FNavCalib then SelectPage(FPageCalib, FNavCalib)
|
||||
else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced)
|
||||
else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT)
|
||||
else if Sender = FNavAlex then SelectPage(FPageAlex, FNavAlex)
|
||||
@@ -783,6 +862,7 @@ begin
|
||||
FPageWaterfall.Visible:= APage = FPageWaterfall;
|
||||
FPageTransmit.Visible := APage = FPageTransmit;
|
||||
FPagePA.Visible := APage = FPagePA;
|
||||
FPageCalib.Visible := APage = FPageCalib;
|
||||
FPageAdvanced.Visible := APage = FPageAdvanced;
|
||||
FPageCAT.Visible := APage = FPageCAT;
|
||||
FPageAlex.Visible := APage = FPageAlex;
|
||||
@@ -795,6 +875,7 @@ begin
|
||||
FNavWaterfall.Active:= ANav = FNavWaterfall;
|
||||
FNavTransmit.Active := ANav = FNavTransmit;
|
||||
FNavPA.Active := ANav = FNavPA;
|
||||
FNavCalib.Active := ANav = FNavCalib;
|
||||
FNavAdvanced.Active := ANav = FNavAdvanced;
|
||||
FNavCAT.Active := ANav = FNavCAT;
|
||||
FNavAlex.Active := ANav = FNavAlex;
|
||||
@@ -1859,6 +1940,469 @@ begin
|
||||
|
||||
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;
|
||||
const
|
||||
MARGIN = 22;
|
||||
@@ -3801,7 +4345,11 @@ begin
|
||||
FVHFCalLabels[i].Caption := 'VHF' + IntToStr(i) + ':';
|
||||
FVHFCalLabels[i].Hint := FVHFCalLabels[i].Caption;
|
||||
end;
|
||||
// Офсеты S-метра на вкладке Calibration — только по включённым слотам.
|
||||
FCalXvtrOn[i] := X.Entries[i].Enabled;
|
||||
FCalXvtrName[i] := X.Entries[i].ButtonText;
|
||||
end;
|
||||
RelayoutCalTab;
|
||||
if FChkXvtrTunPwr <> nil then FChkXvtrTunPwr.Checked := X.UseXVTRTunePower;
|
||||
LoadQOFromConfig; // QO100_SLOT → отдельная страница QO-100
|
||||
finally
|
||||
@@ -3859,7 +4407,7 @@ begin
|
||||
L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM;
|
||||
L.WordWrap := True;
|
||||
|
||||
Grp := MakeGroupPanel(FPageQO, 'Transponder', MARGIN, 100, 560, GRP_TITLE_H + 8 * 30 + 16);
|
||||
Grp := MakeGroupPanel(FPageQO, 'Transponder', MARGIN, 100, 560, GRP_TITLE_H + 9 * 30 + 16);
|
||||
|
||||
Y := GRP_TITLE_H + 8;
|
||||
FQoEn := TFlatCheckBox.Create(Self);
|
||||
@@ -3881,6 +4429,11 @@ begin
|
||||
FQoTxLO := QoFloat(Y, 0.0, 50000.0, 0.001, 6);
|
||||
FQoTxLO.Value := 8089.5;
|
||||
|
||||
// TO Error (Гц): сам транспондер-offset держим стандартным (8089.5 МГц),
|
||||
// а уход конкретного тракта (TCXO uplink-SDR, наводка LNB) правим здесь.
|
||||
Inc(Y, 30); QoLabel('TO Error (Hz)', Y);
|
||||
FQoTxLOErr := QoFloat(Y, -999999.0, 999999.0, 1.0, 0);
|
||||
|
||||
Inc(Y, 30); QoLabel('Downlink begin (MHz)', Y);
|
||||
FQoBegin := QoFloat(Y, 0.0, 50000.0, 0.001, 3);
|
||||
FQoBegin.Value := 10489.5;
|
||||
@@ -3902,7 +4455,7 @@ begin
|
||||
|
||||
FQoInfo := TLabel.Create(Self);
|
||||
FQoInfo.Parent := FPageQO;
|
||||
FQoInfo.SetBounds(MARGIN, 100 + GRP_TITLE_H + 8 * 30 + 28, 700, 52);
|
||||
FQoInfo.SetBounds(MARGIN, 100 + GRP_TITLE_H + 9 * 30 + 28, 700, 52);
|
||||
FQoInfo.Font.Color := CLR_TEXTDIM; FQoInfo.Font.Name := 'Courier New';
|
||||
FQoInfo.Font.Size := 9; FQoInfo.WordWrap := True;
|
||||
finally
|
||||
@@ -3916,7 +4469,7 @@ begin
|
||||
if FQoInfo = nil then Exit;
|
||||
dlMid := (FQoBegin.Value + FQoEnd.Value) / 2.0; // МГц
|
||||
rxIf := dlMid - FQoLnbLO.Value; // RX IF в SDR
|
||||
txF := dlMid - FQoTxLO.Value; // TX uplink
|
||||
txF := dlMid - FQoTxLO.Value + FQoTxLOErr.Value / 1.0e6; // TX uplink + TO Error
|
||||
FQoInfo.Caption := Format(
|
||||
'At downlink %.3f MHz: RX IF = %.3f MHz TX = %.3f MHz',
|
||||
[dlMid, rxIf, txF]);
|
||||
@@ -3931,6 +4484,7 @@ begin
|
||||
FXvtrConfig.Entries[QO100_SLOT].LOOffset := FQoLnbLO.Value * 1.0e6;
|
||||
FXvtrConfig.Entries[QO100_SLOT].LOError := FQoLoErr.Value;
|
||||
FXvtrConfig.Entries[QO100_SLOT].TxLOOffset := FQoTxLO.Value * 1.0e6;
|
||||
FXvtrConfig.Entries[QO100_SLOT].TxLOError := FQoTxLOErr.Value;
|
||||
FXvtrConfig.Entries[QO100_SLOT].FreqBegin := FQoBegin.Value * 1.0e6;
|
||||
FXvtrConfig.Entries[QO100_SLOT].FreqEnd := FQoEnd.Value * 1.0e6;
|
||||
FXvtrConfig.Entries[QO100_SLOT].RXGain := FQoRXGain.Value;
|
||||
@@ -3946,6 +4500,7 @@ begin
|
||||
FQoLnbLO.Value := E.LOOffset / 1.0e6;
|
||||
FQoLoErr.Value := E.LOError;
|
||||
FQoTxLO.Value := E.TxLOOffset / 1.0e6;
|
||||
FQoTxLOErr.Value := E.TxLOError;
|
||||
FQoBegin.Value := E.FreqBegin / 1.0e6;
|
||||
FQoEnd.Value := E.FreqEnd / 1.0e6;
|
||||
FQoRXGain.Value := E.RXGain;
|
||||
|
||||
+31
-2
@@ -191,6 +191,11 @@ type
|
||||
// SliceId — логический id (из RadioController), не WDSP-канал.
|
||||
TOnSliceAudio = procedure(SliceId: Integer; const Left, Right: array of Single;
|
||||
Count: Integer) of object;
|
||||
// Калибровочный офсет спектра/водопада (дБ) для дисплея: PanId 0 = главный,
|
||||
// 1..MAX_PANS-1 = доп. панадаптер. Спрашивается раз в кадр из display-потока:
|
||||
// величина зависит от диапазона/трансвертера, о которых знает только
|
||||
// TRadioController (аналог SMeterCalFor для стрелки).
|
||||
TOnDispCalQuery = function(PanId: Integer): Double of object;
|
||||
// Пиксели спектра/водопада доп. пана — из display-потока (как FOnSpectrum/
|
||||
// FOnWaterfall главного). Внутри колбэка НЕ звать slice/pan API движка.
|
||||
TOnPanPixels = procedure(PanId: Integer; const Pixels: array of Single;
|
||||
@@ -331,6 +336,7 @@ type
|
||||
FOnDemodAudio:TOnAudioReady; // pre-volume/mute tap for digital decoders
|
||||
FOnSpectrum: TOnSpectrumReady;
|
||||
FOnWaterfall: TOnWaterfallReady;
|
||||
FOnDispCal: TOnDispCalQuery;
|
||||
FOnTXIQ: TOnTXIQReady;
|
||||
|
||||
// --- Мультислайсы ---
|
||||
@@ -756,6 +762,8 @@ type
|
||||
property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio;
|
||||
property OnSliceDemodAudio: TOnSliceAudio
|
||||
read FOnSliceDemodAudio write FOnSliceDemodAudio;
|
||||
// Офсет дисплея (дБ) на кадр — см. TOnDispCalQuery. nil = без коррекции.
|
||||
property OnDispCal: TOnDispCalQuery read FOnDispCal write FOnDispCal;
|
||||
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
|
||||
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
||||
// Пиксели доп. панов (display-поток). UI вешает при появлении панов (3.3).
|
||||
@@ -3810,18 +3818,24 @@ var
|
||||
Flag: Integer;
|
||||
i, N, p: Integer;
|
||||
DispID: Integer;
|
||||
Cal: Single;
|
||||
GotSpec, GotWf: Boolean;
|
||||
begin
|
||||
if not FInitialized or not FAnalyzerOpen then Exit;
|
||||
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
|
||||
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;
|
||||
GetPixels(DispID, 0, @PixBuf[0], @Flag);
|
||||
if Flag <> 0 then
|
||||
begin
|
||||
for i := 0 to N - 1 do
|
||||
FSpectrumPixels[i] := PixBuf[i];
|
||||
FSpectrumPixels[i] := PixBuf[i] + Cal;
|
||||
if Assigned(FOnSpectrum) then
|
||||
FOnSpectrum(FSpectrumPixels, N);
|
||||
end;
|
||||
@@ -3831,7 +3845,7 @@ begin
|
||||
if Flag <> 0 then
|
||||
begin
|
||||
for i := 0 to N - 1 do
|
||||
FWaterfallPixels[i] := PixBuf[i];
|
||||
FWaterfallPixels[i] := PixBuf[i] + Cal;
|
||||
if Assigned(FOnWaterfall) then
|
||||
FOnWaterfall(FWaterfallPixels, N);
|
||||
end;
|
||||
@@ -3858,6 +3872,18 @@ begin
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
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
|
||||
FOnPanSpectrum(p, PixBuf, N);
|
||||
if GotWf and Assigned(FOnPanWaterfall) then
|
||||
@@ -3880,6 +3906,9 @@ function TWDSPEngine.GetSMeterDBm: Double;
|
||||
begin
|
||||
// НЕ вызываем GetRXAMeter здесь повторно — значение уже обновлено
|
||||
// в DSP-колбэке. Повторный вызов сбрасывает аккумулятор WDSP → 0.
|
||||
// Калибровочный офсет здесь НЕ прибавляется: он зависит от диапазона/
|
||||
// трансвертера, о которых движок не знает — его добавляет TRadioController
|
||||
// (ReadSMeterDBm / SliceSMeter).
|
||||
Result := FSMeter;
|
||||
end;
|
||||
|
||||
|
||||
+4
-4
@@ -199,7 +199,7 @@ begin
|
||||
'<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>' +
|
||||
'</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="snbBtn">SNB</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("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("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 — фильтровый оверлей
|
||||
'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());}' +
|
||||
|
||||
// Маппинг: 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){' +
|
||||
'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("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("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);' +
|
||||
'paintSM(Number(cur.smeter_dbm||-130));' +
|
||||
'updStatusBar();' +
|
||||
|
||||
+9
-9
@@ -296,7 +296,7 @@ type
|
||||
FDriveLevel: Integer;
|
||||
FOnFreqA: TWebCmdFreqA;
|
||||
FOnAttn: TWebCmdAttn;
|
||||
FAttnIdx: Integer;
|
||||
FAttnDB: Integer; // шаговый аттенюатор ADC, дБ (0..31)
|
||||
FOnTun: TWebCmdTun;
|
||||
FOnFMStep: TWebCmdFMStep;
|
||||
FTuning: Boolean;
|
||||
@@ -406,7 +406,7 @@ type
|
||||
CenterHz: Double; FilterIdx: Integer;
|
||||
VfoB: Double; ActiveVfo: Integer;
|
||||
Transmitting: Boolean; DriveLevel: Integer;
|
||||
AttnIdx: Integer; Tuning: Boolean; Duplex: Boolean;
|
||||
AttnDB: Integer; Tuning: Boolean; Duplex: Boolean;
|
||||
FwdW, SWRV, PAMaxPower: Double;
|
||||
const StatusText, BoardText, IPText, SupplyText, PLLText,
|
||||
RXText, TXText, SeqText: string);
|
||||
@@ -1572,10 +1572,10 @@ begin
|
||||
end
|
||||
else if Cmd = 'attn' then
|
||||
begin
|
||||
Idx := JsonGetInt(Json, 'idx', FAttnIdx);
|
||||
Idx := JsonGetInt(Json, 'db', FAttnDB);
|
||||
if Idx < 0 then Idx := 0;
|
||||
if Idx > 2 then Idx := 2;
|
||||
FStateLock.Enter; FAttnIdx := Idx; FStateLock.Leave;
|
||||
if Idx > 31 then Idx := 31;
|
||||
FStateLock.Enter; FAttnDB := Idx; FStateLock.Leave;
|
||||
if Assigned(FOnAttn) then FOnAttn(Idx);
|
||||
end
|
||||
else if Cmd = 'fm_step' then
|
||||
@@ -1778,7 +1778,7 @@ begin
|
||||
'"wf_agc":%s,"wf_nf":%s,"band_idx":%d,"smeter_dbm":%.1f,' +
|
||||
'"running":%s,"mute":%s,"ctun":%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,' +
|
||||
'"status_text":"%s","board_text":"%s","ip_text":"%s","supply_text":"%s",' +
|
||||
'"pll_text":"%s","rx_text":"%s","tx_text":"%s","seq_text":"%s",' +
|
||||
@@ -1809,7 +1809,7 @@ begin
|
||||
BoolToStr(FConnected, 'true', 'false'),
|
||||
BoolToStr(FTransmitting, 'true', 'false'),
|
||||
FDriveLevel,
|
||||
FAttnIdx,
|
||||
FAttnDB,
|
||||
BoolToStr(FTuning, 'true', 'false'),
|
||||
BoolToStr(FDuplex, 'true', 'false'),
|
||||
FFwdW, FSWR, FPAMaxPower,
|
||||
@@ -1889,7 +1889,7 @@ procedure TWebServer.PushSpectrum(
|
||||
CenterHz: Double; FilterIdx: Integer;
|
||||
VfoB: Double; ActiveVfo: Integer;
|
||||
Transmitting: Boolean; DriveLevel: Integer;
|
||||
AttnIdx: Integer; Tuning: Boolean; Duplex: Boolean;
|
||||
AttnDB: Integer; Tuning: Boolean; Duplex: Boolean;
|
||||
FwdW, SWRV, PAMaxPower: Double;
|
||||
const StatusText, BoardText, IPText, SupplyText, PLLText,
|
||||
RXText, TXText, SeqText: string);
|
||||
@@ -1926,7 +1926,7 @@ begin
|
||||
FActiveVfo := ActiveVfo;
|
||||
FTransmitting := Transmitting;
|
||||
FDriveLevel := DriveLevel;
|
||||
FAttnIdx := AttnIdx;
|
||||
FAttnDB := AttnDB;
|
||||
FTuning := Tuning;
|
||||
FDuplex := Duplex;
|
||||
FFwdW := FwdW;
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<UseVersionInfo Value="True"/>
|
||||
<AutoIncrementBuild Value="True"/>
|
||||
<MinorVersionNr Value="9"/>
|
||||
<BuildNr Value="276"/>
|
||||
<BuildNr Value="277"/>
|
||||
</VersionInfo>
|
||||
<MacroValues Count="106">
|
||||
<MacroValues Count="108">
|
||||
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
||||
@@ -126,6 +126,8 @@
|
||||
<Macro104 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro105 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro106 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro107 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro108 Name="LCLWidgetType" Value="qt6"/>
|
||||
</MacroValues>
|
||||
<BuildModes>
|
||||
<Item Name="Debug" Default="True"/>
|
||||
|
||||
+1
-1
@@ -466,7 +466,7 @@ begin
|
||||
// не обновляет в headless — освежаем здесь, как GUI-таймер (MainForm). Без
|
||||
// этого PushState слал бы клиентам залипший -130 dBm.
|
||||
if FCtrl.FRunning and FCtrl.FWDSPReady then
|
||||
FCtrl.FLastSMeter := FCtrl.FDSPEngine.GetSMeterDBm;
|
||||
FCtrl.FLastSMeter := FCtrl.ReadSMeterDBm;
|
||||
FWebAd.PushState; // исходящее зеркало состояния/спектра
|
||||
lastPush := nowMs;
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user