feature(calibration): офсет S-метра на диапазон и на трансвертер, раздельно по железу

Вместо одного офсета на всё устройство — таблицы «на диапазон» и «на слот
трансвертера»; состав таблиц задаёт активное железо.

- Settings: SMeterOffsetDB -> SMeterBandDB[12] / SMeterXvtrDB[8] (JSON
  cal_smeter_band_i / cal_smeter_xvtr_i). Легаси-ключ cal_smeter_db становится
  дефолтом всех слотов — показания после обновления не меняются. Разделение
  Pluto/openHPSDR даёт сама секция JSON: она ключуется по MAC устройства
  (у Pluto — synthetic MAC из serial).
- Офсет убран из WDSPEngine (FSMeterCalDB): движок не знает ни диапазонов, ни
  трансвертеров. Теперь TRadioController.SMeterCalFor(VisibleHz) — окно
  включённого XVTR-слота приоритетнее диапазона, индекс диапазона по плану
  железа (FreqToBandIdx с IsPluto); ReadSMeterDBm для главного S-метра
  (MainForm + ewsdrd) и SliceSMeter по TargetHz слайса — паны N на других
  диапазонах считаются по своему банду, а не по главному.
- SettingsForm: таблицы на 12 слотов, подписи/видимость/раскладку ставит
  RelayoutCalTab; SetCalibrationBackend(IsPluto) вызывается рядом с
  SetAntennaBackend. У openHPSDR — HF-план, у Pluto — VHF/UHF-план и скрытые
  группы Power/SWR detector и Supply V/A (нет Alex-моста и его телеметрии).
  Слоты трансвертеров показываются только включённые, с их именами.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 11:02:04 +03:00
co-authored by Claude Opus 4.8
parent ced42fbac9
commit 797bfc2e7c
7 changed files with 301 additions and 73 deletions
+223 -52
View File
@@ -153,14 +153,31 @@ type
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..10] of TFlatFloatSpinEdit; // множитель FWD/REV на банд
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; // датчик тока: мВ/А
FEdSMeterOffset: TFlatFloatSpinEdit; // офсет S-метра, дБ
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;
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;
@@ -352,6 +369,7 @@ type
procedure BuildRX1Tab;
procedure BuildPATab;
procedure BuildCalibrationTab;
procedure RelayoutCalTab;
procedure CollectCalFromUI;
procedure OnCalAnyChange(Sender: TObject);
procedure BuildCATTab;
@@ -463,6 +481,8 @@ type
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);
// Таблица разъёмов + имена/видимость слотов трансвертеров.
@@ -569,6 +589,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);
@@ -1888,20 +1923,11 @@ end;
procedure TSettingsForm.BuildCalibrationTab;
// Вкладка Calibration (паритет с Thetis-Enhanced): модель детектора мощности
// Alex-моста + множитель FWD/REV на банд (Det. Cal.), калибровка напряжения/
// тока питания (PAVoltCal/AmpVoff/AmpSens) и офсет S-метра.
// тока питания (PAVoltCal/AmpVoff/AmpSens) и офсет S-метра на диапазон и на
// слот трансвертера. Позиции/подписи/видимость band-строк ставит RelayoutCalTab
// под активное железо — здесь только создание контролов.
const
MARGIN = 22;
GRP_PAD = 18;
LBL_W = 54;
ED_W = 72;
COL_W = LBL_W + ED_W + 14;
COLS = 4;
ROW_H = 32;
R1 = 42;
GRP1_H = 236;
GRP2_H = 156;
GRP3_H = 92;
VA_LBL_W = 220;
GRP3_H0 = 200; // стартовая высота S-метра, уточняется в RelayoutCalTab
function MakeCalSpin(AParent: TWinControl; ALeft, ATop: Integer;
AMin, AMax, AInc: Double; ADec: Integer;
@@ -1910,7 +1936,7 @@ const
begin
E := TFlatFloatSpinEdit.Create(Self);
E.Parent := AParent;
E.SetBounds(ALeft, ATop, ED_W, BTN_H + 2);
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;
@@ -1927,18 +1953,24 @@ const
var
Grp: TPanel;
Lbl: TLabel;
i, r, c, X, Y, GTop: Integer;
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(MARGIN, 18, 400, 28);
SetBounds(CAL_MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
@@ -1949,7 +1981,7 @@ begin
begin
Parent := FPageCalib;
Caption := 'Power/SWR detector, supply voltage/current and S-meter calibration.';
SetBounds(MARGIN, 48, 620, 20);
SetBounds(CAL_MARGIN, 48, 620, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
@@ -1957,10 +1989,12 @@ begin
// ── Power / SWR detector ────────────────────────────────────────────────
GTop := 86;
Grp := MakeGroupPanel(FPageCalib, 'Power / SWR detector', MARGIN, GTop, 700, GRP1_H);
FCalGrpDet := MakeGroupPanel(FPageCalib, 'Power / SWR detector',
CAL_MARGIN, GTop, CAL_GRP_W, CAL_GRP1_H);
Grp := FCalGrpDet;
Lbl := MakeLbl(Grp, 'Detector model', GRP_PAD, R1 + 6, 132);
FCmbPwrDetModel := MakeCombo(Grp, GRP_PAD + 150, R1, 280, OnCalAnyChange);
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');
@@ -1970,58 +2004,187 @@ begin
Lbl := MakeLbl(Grp,
'FWD/REV multiplier per band: 1.00 = no correction, >1 if meter under-reads '
+ '(e.g. 88 W shown at real 100 W -> 1.14). SWR is not affected.',
GRP_PAD, R1 + ROW_H + 10, 650);
CAL_PAD, CAL_R1 + CAL_ROW_H + 10, 650);
for i := 0 to 10 do
for i := 0 to CFG_BAND_COUNT - 1 do
begin
r := i div COLS;
c := i mod COLS;
X := GRP_PAD + c * (COL_W + 4);
Y := R1 + ROW_H + 46 + r * ROW_H;
Lbl := MakeLbl(Grp, PA_BAND_NAMES[i] + ':', X, Y + 5, LBL_W);
FEdPADetCal[i] := MakeCalSpin(Grp, X + LBL_W, Y, 0.50, 2.00, 0.01, 2, 1.00);
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 + GRP1_H + 14;
Grp := MakeGroupPanel(FPageCalib, 'Supply voltage / current', MARGIN, GTop, 700, GRP2_H);
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)', GRP_PAD, R1 + 6, VA_LBL_W);
FEdSupplyVoltCal := MakeCalSpin(Grp, GRP_PAD + VA_LBL_W + 10, R1, 0.100, 5.000, 0.001, 3, 1.000);
Lbl := MakeLbl(Grp, '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)', GRP_PAD, R1 + ROW_H + 6, VA_LBL_W);
FEdAmpVoff := MakeCalSpin(Grp, GRP_PAD + VA_LBL_W + 10, R1 + ROW_H, 0.0, 2000.0, 1.0, 0, 360.0);
Lbl := MakeLbl(Grp, 'Current sensor 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)', GRP_PAD, R1 + 2 * ROW_H + 6, VA_LBL_W);
FEdAmpSens := MakeCalSpin(Grp, GRP_PAD + VA_LBL_W + 10, R1 + 2 * ROW_H, 1.0, 1000.0, 0.1, 1, 120.0);
Lbl := MakeLbl(Grp, '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)',
GRP_PAD + VA_LBL_W + 10 + ED_W + 14, R1 + ROW_H + 6, 320);
CAL_PAD + CAL_VA_LBL_W + 10 + CAL_ED_W + 14,
CAL_R1 + CAL_ROW_H + 6, 320);
// ── S-meter ─────────────────────────────────────────────────────────────
GTop := GTop + GRP2_H + 14;
Grp := MakeGroupPanel(FPageCalib, 'S-meter', MARGIN, GTop, 700, GRP3_H);
GTop := GTop + CAL_GRP2_H + 14;
FCalGrpSM := MakeGroupPanel(FPageCalib, 'S-meter',
CAL_MARGIN, GTop, CAL_GRP_W, GRP3_H0);
Grp := FCalGrpSM;
Lbl := MakeLbl(Grp, 'Offset (dB), added to all S-meter readings', GRP_PAD, R1 + 6, VA_LBL_W);
FEdSMeterOffset := MakeCalSpin(Grp, GRP_PAD + VA_LBL_W + 10, R1, -30.0, 30.0, 0.1, 1, 0.0);
FLblSMBandHdr := MakeLbl(Grp,
'Offset (dB) added to S-meter readings — per band (+ = meter reads higher).',
CAL_PAD, 30, 650);
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;
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, BandRows, XvtrRows, Slot, H: Integer;
Nm: string;
begin
// Гард: вкладка ещё строится (или LoadXvtrSettings пришёл раньше неё).
if (FCalGrpDet = nil) or (FCalGrpVA = nil) or (FCalGrpSM = nil)
or (FLblSMXvtrHdr = nil) then Exit;
// ---- строки диапазонов: позиция по индексу плана, пустые слоты скрыты ----
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_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;
X := CAL_PAD + c * (CAL_SM_LBL_W + CAL_ED_W + 14);
Y := 52 + r * CAL_ROW_H;
if FLblSMBand[i] <> nil then
begin
FLblSMBand[i].Caption := Nm + ':';
FLblSMBand[i].SetBounds(X, Y + 5, CAL_SM_LBL_W, 18);
FLblSMBand[i].Visible := Nm <> '';
end;
if FEdSMBand[i] <> nil then
begin
FEdSMBand[i].SetBounds(X + CAL_SM_LBL_W, Y, CAL_ED_W, BTN_H + 2);
FEdSMBand[i].Visible := Nm <> '';
end;
if Nm <> '' then BandRows := r + 1;
end;
// ---- строки трансвертеров: только включённые слоты, без дырок ----
Y := 52 + BandRows * CAL_ROW_H + 6;
FLblSMXvtrHdr.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);
FLblSMXvtr[i].Caption := Nm + ':';
FLblSMXvtr[i].Hint := 'XVTR' + IntToStr(i) + ' (' + Nm + ')';
FLblSMXvtr[i].SetBounds(X, Y + 24 + r * CAL_ROW_H + 5, CAL_SM_LBL_W, 18);
FEdSMXvtr[i].SetBounds(X + CAL_SM_LBL_W, Y + 24 + r * CAL_ROW_H,
CAL_ED_W, BTN_H + 2);
Inc(Slot);
end;
FLblSMXvtr[i].Visible := FCalXvtrOn[i];
FEdSMXvtr[i].Visible := FCalXvtrOn[i];
end;
XvtrRows := (Slot + CAL_COLS - 1) div CAL_COLS;
FLblSMXvtrHdr.Visible := Slot > 0;
if Slot > 0 then H := Y + 24 + XvtrRows * CAL_ROW_H + 12
else H := Y + 12;
// ---- вертикальная укладка групп ----
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, H);
end;
procedure TSettingsForm.SetCalibrationBackend(IsPluto: Boolean);
begin
FCalIsPluto := IsPluto;
RelayoutCalTab;
end;
procedure TSettingsForm.CollectCalFromUI;
var i: Integer;
begin
FCalConfig.PwrDetModel := FCmbPwrDetModel.ItemIndex;
// UI редактирует HF-банды 0..10; слот 11 (пустой в HF-плане) не трогаем.
for i := 0 to 10 do
FCalConfig.PADetCal[i] := FEdPADetCal[i].Value;
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;
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
if FEdSMXvtr[i] <> nil then FCalConfig.SMeterXvtrDB[i] := FEdSMXvtr[i].Value;
FCalConfig.SupplyVoltCal := FEdSupplyVoltCal.Value;
FCalConfig.AmpVoffMV := FEdAmpVoff.Value;
FCalConfig.AmpSensMVA := FEdAmpSens.Value;
FCalConfig.SMeterOffsetDB := FEdSMeterOffset.Value;
end;
procedure TSettingsForm.OnCalAnyChange(Sender: TObject);
@@ -2059,12 +2222,16 @@ begin
try
FCalConfig := C;
FCmbPwrDetModel.ItemIndex := EnsureRange(C.PwrDetModel, 0, 3);
for i := 0 to 10 do
FEdPADetCal[i].Value := C.PADetCal[i];
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];
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
if FEdSMXvtr[i] <> nil then FEdSMXvtr[i].Value := C.SMeterXvtrDB[i];
FEdSupplyVoltCal.Value := C.SupplyVoltCal;
FEdAmpVoff.Value := C.AmpVoffMV;
FEdAmpSens.Value := C.AmpSensMVA;
FEdSMeterOffset.Value := C.SMeterOffsetDB;
finally
FLoading := False;
end;
@@ -4012,7 +4179,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