feat(ui): настройки показывают только то, что есть у активного железа

В TBackendCaps восемь флагов заполнялись обоими бэкендами и не читались нигде,
а в ApplyBackendCapsToUI на этом месте стояла расписка «(TODO) гейтинг полей,
которых нет у бэкенда». Из-за этого на AD936x оператору предлагали крутить то,
чего у платы нет, — и хуже всего страница PA: калибровка драйва по диапазонам
там просто ничего не делает (мощность задаётся железной аттенюацией, drive-байта
нет вовсе), но выглядела рабочей.

Теперь гейт идёт по возможностям, а не по виду устройства:
- HasPA — потолок мощности и калибровки драйва (HF + трансвертеры) против
  «TX att @100% drive»; заголовок и подзаголовок страницы следуют содержимому;
- HasHWMic — строка микрофонного разъёма (DUC Specific байт 50). Mic gain
  остаётся: он идёт в WDSP и работает с любым источником, включая звук с ПК,
  поэтому поднимается в освободившуюся строку;
- HasWideband — галки широкополосного АЦП; сам режим принудительно гасится,
  иначе сохранённый в конфиге wideband оставлял бы пустую панель без способа
  её выключить;
- HasDitherRandom — группа ADC целиком, Web Server встаёт на её место.
Вкладка QO-100 убрана на openHPSDR: TRadioController.InQO100 требует IsPluto,
то есть транспондер там недостижим. Симметрично OC Control, которого нет у
AD936x.

Измеритель: TSMeterView.HasPATelemetry. Без моста детектора FLastFwdW/FLastSWR
стоят на значениях инициализации, и «0W / SWR 1.0» на передаче — не показание,
а выдумка. Шкала рисуется пустой: деления и рамка на месте, заливки нет, ватты
на делениях не подписаны (они считаются от Max power, которого у AD936x нет).

★Отдельно — дефект раскладки, который всё это вскрыло. Перекладка страницы
настроек ломается в двух случаях, и оба случались при смене трансивера на живой
программе (Stop/Discover/Connect/Start), а не при старте:

1. Страница ПРОКРУЧЕНА. TScrollBox двигает детей, и SetBounds задаёт координаты
   относительно прокрученного клиента: всё уезжало вверх, а первая группа
   оставалась на месте — под переключателями Profile/EQ/Hardware зияла дыра.
   Лечение — сброс прокрутки первой строкой (так уже делал RelayoutCalTab).
2. Страница СПРЯТАНА. Тогда сброс не доходит до виджета, а сохранённое смещение
   возвращается при показе — и дыра та же. А попадает перекладка почти всегда
   именно на спрятанную страницу: гейты отрабатывают при открытии окна, когда
   активна одна вкладка, а до Transmit/Calibration оператор доходит потом.
   Лечение — перекладка спрятанной страницы взводит флаг и выходит, а SelectPage
   исполняет её в момент показа (RunDueRelayout). Гард стоит внутри каждой
   Relayout*, а не у вызывающих: у RelayoutCalTab их два, и второй
   (LoadXvtrSettings) приходит как раз на спрятанную страницу.

Проверено офскрин-рендером окна настроек в обе стороны (openHPSDR ↔ AD936x) для
Transmit/Power Amplifier/Calibration/Advanced, в том числе на прокрученной и
спрятанной странице. При полном железе координаты совпадают со старыми.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M37eCTZZ9aqppKWkZRxXkP
This commit is contained in:
2026-08-25 10:16:31 +03:00
co-authored by Claude Opus 5
parent eedaffad78
commit f99443620a
4 changed files with 435 additions and 27 deletions
+30 -2
View File
@@ -4649,10 +4649,12 @@ begin
end;
procedure TMainForm.ApplyBackendCapsToUI;
// Пресеты sample-rate оверлея из Caps (Pluto >560k) + (TODO) гейтинг полей,
// которых нет у бэкенда (PA/SWR/Alex/Wideband).
// Пресеты sample-rate оверлея из Caps (Pluto >560k) + гейтинг того, чего у
// бэкенда физически нет. Настройки гейтятся при открытии окна (SF.Set*Backend
// в BtnSettingsClick), здесь — то, что видно в главном окне.
var
Caps: TBackendCaps;
i: Integer;
begin
Caps := FController.BackendCaps;
if Assigned(FSampleRateOverlay) then
@@ -4665,6 +4667,21 @@ begin
PushBandsToWeb; // band-план бэкенда → web (HF ↔ VHF/UHF)
// Отдельный Pluto RF-AGC блок под AGC: показываем и сдвигаем панели ниже.
// Широкополосный АЦП есть только у openHPSDR. Галка на такой машине из
// настроек убрана, поэтому включённой её оставлять нельзя: панель осталась бы
// пустой, а выключить её стало бы нечем.
if (not Caps.HasWideband) and FController.FShowWideband then
ApplyVisibility(FController.FShowSpectrum, FController.FShowWaterfall,
False, FController.FWidebandFill, FController.FSpectrumFill);
// Измеритель PWR/SWR: без моста детектора (AD936x) FLastFwdW/FLastSWR стоят
// на инициализации, и «0W / SWR 1.0» на передаче были бы выдумкой — шкала
// рисуется пустой.
if FSpecView <> nil then FSpecView.HasPATelemetry := Caps.HasPA;
for i := 0 to High(FPans) do
if (FPans[i] <> nil) and (FPans[i].View <> nil) then
FPans[i].View.HasPATelemetry := Caps.HasPA;
LayoutLeftPanel(Caps.HasHWGain);
if Caps.HasHWGain then UpdateRxGainUI;
UpdateRxMuteButton; // RX MUTE — видимость по Pluto + full-duplex (если QO-100 уже активен)
@@ -7633,6 +7650,7 @@ begin
P.View.SpecGridStep := FController.FSpecGridStep;
P.View.WfFrameInterval := EffectiveWfInterval;
P.View.PAMaxPower := FController.FPAMaxPower;
P.View.HasPATelemetry := FController.BackendCaps.HasPA;
P.View.CenterFreq := Freq;
P.View.SpanHz := FController.PanDDCRateKHz(PanId) * 1000.0;
// VFO/TX-маркеры главного на доп. пане не рисуем — уводим за пределы окна.
@@ -9767,6 +9785,16 @@ begin
SF.LoadOCSettings(FController.FOCSettings);
// Вкладка OC Control — только openHPSDR, у Pluto/AD936x нет такого выхода.
SF.SetOCBackend(FController.BackendCaps.Kind = bkPluto);
// Вкладка QO-100 — ровно наоборот: транспондер живёт только на AD936x
// (TRadioController.InQO100 требует IsPluto).
SF.SetQOBackend(FController.BackendCaps.Kind = bkPluto);
// Остальные гейты — по объявленным возможностям бэкенда, а не по его виду:
// мост детектора мощности и drive-байт (PA), микрофонный разъём трансивера,
// широкополосный АЦП, dither/random. Всё это есть у openHPSDR и нет у AD936x.
SF.SetPABackend(FController.BackendCaps.HasPA);
SF.SetTXMicBackend(FController.BackendCaps.HasHWMic);
SF.SetWidebandBackend(FController.BackendCaps.HasWideband);
SF.SetADCBackend(FController.BackendCaps.HasDitherRandom);
SF.LoadPASettings(FController.FPAMaxPower, FController.FPABandCal);
SF.LoadCalibration(FController.FCal);
SF.LoadPlutoTxMaxAtt(FController.FPlutoTxMaxAttDb);
+38 -14
View File
@@ -30,6 +30,11 @@ type
FLastFwdW: Double;
FLastSWR: Double;
FPAMaxPower: Double;
// Есть ли у железа мост детектора мощности (Caps.HasPA). У AD936x его нет:
// FLastFwdW/FLastSWR так и стоят на значениях инициализации, и «0W / SWR
// 1.0» на передаче — не показание, а выдумка. Шкала в этом случае рисуется
// пустой: деления есть, заливки и цифр нет.
FHasPATelemetry: Boolean;
FTransmitting: Boolean;
FPbSMeterRight: TPaintBox;
// Масштаб канвы: 2 на Retina, 1 на Windows/Linux. Битмап растёт во столько
@@ -52,6 +57,7 @@ type
property LastFwdW: Double read FLastFwdW write FLastFwdW;
property LastSWR: Double read FLastSWR write FLastSWR;
property PAMaxPower: Double read FPAMaxPower write FPAMaxPower;
property HasPATelemetry: Boolean read FHasPATelemetry write FHasPATelemetry;
property Transmitting: Boolean read FTransmitting write FTransmitting;
property PbSMeterRight: TPaintBox write FPbSMeterRight;
@@ -76,6 +82,7 @@ begin
FLastFwdW := 0;
FLastSWR := 1;
FPAMaxPower := 100.0;
FHasPATelemetry := True;
FTransmitting := False;
FScale := 1;
end;
@@ -358,9 +365,17 @@ begin
W := R.Right - R.Left; H := R.Bottom - R.Top;
if (W < 80 * Sc) or (H < 16 * Sc) then Exit;
SWRHigh := FLastSWR > SWR_HIGH_THRESH;
LblPwr := Format('%.0fW', [FLastFwdW]);
LblSWR := Format('SWR:%.1f', [FLastSWR]);
SWRHigh := FHasPATelemetry and (FLastSWR > SWR_HIGH_THRESH);
if FHasPATelemetry then
begin
LblPwr := Format('%.0fW', [FLastFwdW]);
LblSWR := Format('SWR:%.1f', [FLastSWR]);
end
else
begin
LblPwr := ''; // измерять нечем — показания не выдумываем
LblSWR := '';
end;
ACanvas.Font.Name := 'Courier New'; ACanvas.Font.Style := [fsBold];
ACanvas.Font.Size := 10 * Sc;
@@ -394,18 +409,24 @@ begin
FillBar(BX, SY1, S_WarnX, SY2, CLR_BAR_GREEN);
FillBar(S_WarnX, SY1, BX + BW, SY2, CLR_BAR_OVER);
if FPAMaxPower > 0 then PwrPct := FLastFwdW / FPAMaxPower else PwrPct := 0;
PBarEnd := PwrToX(PwrPct);
FillBar(BX, PY1 + Sc, PBarEnd, PY2 - Sc, SM_CLR_METER_ON);
SBarEnd := SWRToX(FLastSWR);
if not SWRHigh then
FillBar(BX, SY1 + Sc, SBarEnd, SY2 - Sc, SM_CLR_AMBER)
else
if FHasPATelemetry then
begin
FillBar(BX, SY1 + Sc, Min(S_WarnX, SBarEnd), SY2 - Sc, SM_CLR_AMBER);
if SBarEnd > S_WarnX then
FillBar(S_WarnX, SY1 + Sc, SBarEnd, SY2 - Sc, CLR_SWR_HIGH);
if FPAMaxPower > 0 then PwrPct := FLastFwdW / FPAMaxPower else PwrPct := 0;
PBarEnd := PwrToX(PwrPct);
FillBar(BX, PY1 + Sc, PBarEnd, PY2 - Sc, SM_CLR_METER_ON);
end;
if FHasPATelemetry then
begin
SBarEnd := SWRToX(FLastSWR);
if not SWRHigh then
FillBar(BX, SY1 + Sc, SBarEnd, SY2 - Sc, SM_CLR_AMBER)
else
begin
FillBar(BX, SY1 + Sc, Min(S_WarnX, SBarEnd), SY2 - Sc, SM_CLR_AMBER);
if SBarEnd > S_WarnX then
FillBar(S_WarnX, SY1 + Sc, SBarEnd, SY2 - Sc, CLR_SWR_HIGH);
end;
end;
VLine(S_WarnX, SY1, SY2, FTheme.SMeterDivider);
@@ -437,6 +458,9 @@ begin
begin
X := PwrToX(PWR_MARKS[i]);
VLine(X, PY1 - TICK_LONG * Sc, PY1 - Sc, CLR_TICK_WHITE);
// Ватты на делениях считаются от Max power, а он есть только там, где есть
// детектор. Без него оставляем голые деления: подписать их нечем.
if not FHasPATelemetry then Continue;
Lbl := Format('%.0f', [PWR_MARKS[i] * FPAMaxPower]);
TW := ACanvas.TextWidth(Lbl);
ACanvas.Font.Color := CLR_SCALE_DBM;
+361 -11
View File
@@ -210,6 +210,17 @@ type
FEdBandCal: array[0..10] of TFlatFloatSpinEdit;
FEdVHFCal: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit;
FVHFCalLabels: array[0..CFG_XVTR_COUNT-1] of TLabel;
// Раскладку страницы PA ставит RelayoutPATab под активное железо: у AD936x
// нет ни моста детектора, ни drive-байта — мощность там задаётся
// аттенюацией, поэтому из всей страницы остаётся один потолок.
FPAGrpPower: TPanel;
FPAGrpVHF: TPanel;
FLblPAHint: TLabel;
FLblPAMaxPower: TLabel;
FLblPlutoTxAtt: TLabel;
FLblPABandHdr: TLabel;
FPABandLabels: array[0..10] of TLabel;
FPAHasPA: Boolean;
// ---- Calibration tab controls ----
// Таблицы «на диапазон» строятся на 12 слотов (максимум плана); подписи,
@@ -242,6 +253,7 @@ type
FLblDispBandHdr: TLabel;
FLblDispXvtrHdr: TLabel;
FBtnDispFromSM: TFlatButton; // «= S-meter» — копия таблицы
FLblCalHint: TLabel; // подзаголовок страницы, зависит от железа
FCalGrpFreq: TPanel; // Reference frequency
FEdFreqCalPPM: TFlatFloatSpinEdit; // калибровка опорника, ppm
FCalIsPluto: Boolean; // активное железо — AD936x
@@ -274,7 +286,16 @@ type
FChkTXMicPTT: TFlatCheckBox;
FCmbTXMicTR: TFlatComboBox; // "Tip" / "Ring" (только Orion)
FLblTipRing: TLabel; // метка (только Orion)
FLblMicJack: TLabel; // метка "Mic jack"
FLblMicGain: TLabel; // метка "Mic gain (dB)"
FEdTXMicGain: TFlatFloatSpinEdit;
// Группы подвкладки Profile — их двигает RelayoutTXProfileSub, когда у
// железа нет микрофонного разъёма (AD936x: звук только с ПК).
FTXGrpMic: TPanel;
FTXGrpFilt: TPanel;
FTXGrpDyn: TPanel;
FTXGrpPwr: TPanel;
FTXHasHWMic: Boolean;
FEdTXAttOnTX: TFlatSpinEdit;
// Tune (TUN button)
FEdTUNLevel: TFlatSpinEdit;
@@ -470,6 +491,21 @@ type
// ---- Advanced tab controls ----
FChkDither: TFlatCheckBox;
FChkRandom: TFlatCheckBox;
// Группа ADC прячется целиком у AD936x (SetADCBackend), Web Server встаёт
// на её место — иначе страница начиналась бы с пустоты.
FAdvGrpADC: TPanel;
FAdvGrpWeb: TPanel;
FLblAdvHint: TLabel;
FAdvHasADC: Boolean;
// ★Перекладка страницы возможна только когда страница ВИДИМА: сброс
// прокрутки не доходит до спрятанного виджета, а сохранённое смещение
// возвращается при показе — и блоки разъезжаются. Поэтому перекладка
// спрятанной страницы лишь взводит свой флаг, а исполняется из SelectPage
// в момент показа (RunDueRelayout).
FPARelayoutDue: Boolean;
FTXRelayoutDue: Boolean;
FCalRelayoutDue: Boolean;
FAdvRelayoutDue: Boolean;
FChkWebEnabled: TFlatCheckBox;
FEdWebPort: TFlatSpinEdit;
FEdWebBind: TFlatEdit;
@@ -531,7 +567,12 @@ type
procedure BuildRX1Tab;
procedure BuildPATab;
procedure BuildCalibrationTab;
function GroupTitle(Grp: TPanel): TLabel;
procedure RunDueRelayout(APage: TWinControl);
procedure RelayoutCalTab;
procedure RelayoutPATab;
procedure RelayoutTXProfileSub;
procedure RelayoutAdvancedTab;
// Раскладка блока офсетов «на диапазон + на трансвертер» внутри группы
// (общая для S-метра и спектра). Возвращает нужную высоту группы.
function LayoutCalOffsets(const BandLbl: array of TLabel;
@@ -694,6 +735,9 @@ type
procedure LoadCalibration(const C: TCalibration);
// Железо вкладки Calibration: band-план и состав групп (Pluto/openHPSDR).
procedure SetCalibrationBackend(IsPluto: Boolean);
// Мост детектора мощности и drive-байт есть только у openHPSDR: у AD936x
// мощность = железная аттенюация, и вся калибровка по диапазонам мертва.
procedure SetPABackend(HasPA: Boolean);
// Вкладка Antenna: Alex-таблица (openHPSDR) либо таблица разъёмов AD936x.
procedure SetAntennaBackend(IsAD936x, HasAntSwitch: Boolean);
// Таблица разъёмов + имена/видимость слотов трансвертеров.
@@ -716,6 +760,11 @@ type
procedure LoadCWSettings(const C: TCWSettings);
// Есть ли у активного железа кейер в прошивке (openHPSDR — да, AD936x — нет).
procedure SetCWBackend(HasFWKeyer: Boolean);
// Микрофонный разъём трансивера (DUC Specific байт 50) — только openHPSDR.
procedure SetTXMicBackend(HasHWMic: Boolean);
// Широкополосный АЦП и dither/random — тоже свойства openHPSDR.
procedure SetWidebandBackend(HasWideband: Boolean);
procedure SetADCBackend(HasDitherRandom: Boolean);
// Список TX-профилей + активный + drive активного (ползунок DRV живёт в
// главном окне, здесь показываем его значение как часть профиля).
procedure LoadTXProfiles(const Names: array of string;
@@ -730,6 +779,8 @@ type
procedure LoadOCSettings(const O: TOCSettings);
// Вкладка OC Control — только openHPSDR (у Pluto/AD936x нет такого выхода).
procedure SetOCBackend(IsPluto: Boolean);
// Вкладка QO-100 — наоборот, только AD936x (см. TRadioController.InQO100).
procedure SetQOBackend(IsPluto: Boolean);
procedure LoadWfAGCNF(AGC, NF: Boolean);
procedure LoadWfRender(Palette: Integer; Gamma: Double;
ResLimit, SpeedDiv: Integer);
@@ -1029,6 +1080,26 @@ begin
Divider.Color := CLR_BORDER;
end;
procedure TSettingsForm.RunDueRelayout(APage: TWinControl);
// Зовётся из SelectPage сразу после того, как страница стала видимой: только
// теперь у её TScrollBox осмысленная прокрутка, и перекладку можно исполнить.
begin
if (APage = FPagePA) and FPARelayoutDue then RelayoutPATab
else if (APage = FPageTransmit) and FTXRelayoutDue then RelayoutTXProfileSub
else if (APage = FPageCalib) and FCalRelayoutDue then RelayoutCalTab
else if (APage = FPageAdvanced) and FAdvRelayoutDue then RelayoutAdvancedTab;
end;
function TSettingsForm.GroupTitle(Grp: TPanel): TLabel;
// Подпись группы MakeGroupPanel создаёт первой — до разделителя и до всего
// содержимого, — поэтому она всегда Controls[0]. Нужна там, где заголовок
// зависит от железа (страница PA).
begin
Result := nil;
if (Grp <> nil) and (Grp.ControlCount > 0) and (Grp.Controls[0] is TLabel) then
Result := TLabel(Grp.Controls[0]);
end;
function TSettingsForm.MakeScrollPage: TScrollBox;
begin
Result := TScrollBox.Create(Self);
@@ -1556,6 +1627,9 @@ begin
// Скрытые alClient-страницы LCL не всегда перекладывает до показа.
// После переключения берём уже актуальный ClientWidth выбранной страницы.
RelayoutResponsiveCards;
// Отложенная перекладка под железо — теперь страница видима, и сброс
// прокрутки внутри неё наконец доходит до виджета.
RunDueRelayout(APage);
UpdateSettingsScrollBar;
end;
@@ -2019,9 +2093,10 @@ begin
// шагом. Connector скрыт на не-Orion и потому делит строку с Mic gain —
// спрятанная пара не оставляет дыры.
Grp := MakeGroupPanel(FTXSubPage[0], 'Microphone', MARGIN, 0, GRP_W, 134);
FTXGrpMic := Grp;
// Row 0: Mic jack | Boost (Mic In) либо Line In gain (Line In) | Bias | PTT
MakeLbl(Grp, 'Mic jack', PAD, R1 + 5, LW);
FLblMicJack := MakeLbl(Grp, 'Mic jack', PAD, R1 + 5, LW);
FCmbTXMicJack := MkCmb(Grp, CX, R1, 110);
FCmbTXMicJack.Items.Add('Mic In');
FCmbTXMicJack.Items.Add('Line In');
@@ -2037,7 +2112,10 @@ begin
FChkTXMicPTT := MkChk(Grp, PAD + 596, R1, 130, 'Mic-jack PTT');
// Row 1: Mic gain | Connector Tip/Ring (только Orion boards 4 и 5)
MakeLbl(Grp, 'Mic gain (dB)', PAD, R1 + STEP + 5, LW);
// ★Mic gain — единственное здесь, что не про разъём: он идёт в WDSP
// (SetMicGain) и работает с любым источником, включая звук с ПК. При
// скрытом разъёме RelayoutTXProfileSub поднимает его в первую строку.
FLblMicGain := MakeLbl(Grp, 'Mic gain (dB)', PAD, R1 + STEP + 5, LW);
FEdTXMicGain := MkFloat(Grp, CX, R1 + STEP, 10.0, -20.0, 60.0, 0.5);
FLblTipRing := MakeLbl(Grp, 'Connector', PAD + 302, R1 + STEP + 5, 76);
@@ -2051,6 +2129,7 @@ begin
// ---- Кромки TX-фильтра (FIR/фаза/окно — на вкладке Hardware) ----
Y := 134 + 12;
Grp := MakeGroupPanel(FTXSubPage[0], 'TX Filter (bandpass)', MARGIN, Y, GRP_W, 108);
FTXGrpFilt := Grp;
MakeLbl(Grp, 'Low cut (Hz)', PAD, R1 + 5, LW);
FEdTXFiltLow := MkSpin(Grp, CX, R1, 0, 8000, 200);
@@ -2063,6 +2142,7 @@ begin
// ---- Динамическая обработка ----
Inc(Y, 108 + 12);
Grp := MakeGroupPanel(FTXSubPage[0], 'Dynamic processing', MARGIN, Y, GRP_W, 220);
FTXGrpDyn := Grp;
FChkTXCompOn := MkChk(Grp, PAD, R1, 220, 'Compressor');
MakeLbl(Grp, 'Gain (dB)', PAD + 230, R1 + 5, 80);
@@ -2089,6 +2169,7 @@ begin
// ---- Мощность и несущая AM (тоже часть профиля) ----
Inc(Y, 220 + 12);
Grp := MakeGroupPanel(FTXSubPage[0], 'Power / AM carrier', MARGIN, Y, GRP_W, 148);
FTXGrpPwr := Grp;
MakeLbl(Grp, 'Tune level (%)', PAD, R1 + 5, LW);
FEdTUNLevel := MkSpin(Grp, CX, R1, 0, 100, 10);
@@ -2101,6 +2182,7 @@ begin
R1 + STEP + 28, 26);
FTXSubPage[0].Height := DpiScale(Y + 148 + 12);
FTXHasHWMic := True; // до подключения показываем полный (openHPSDR) вид
// =========================================================================
// Подвкладка 1 — EQ (тоже профильный, но занимает целый экран)
@@ -2566,12 +2648,142 @@ procedure TSettingsForm.UpdateMicJackVisibility;
var LineIn: Boolean;
begin
if FCmbTXMicJack = nil then Exit;
// Без разъёма прятать нечего и показывать нечего: вся строка снята
// RelayoutTXProfileSub, и вернуть её отсюда было бы возвратом дыры.
if not FTXHasHWMic then Exit;
LineIn := FCmbTXMicJack.ItemIndex = 1;
if FChkTXMicBoost <> nil then FChkTXMicBoost.Visible := not LineIn;
if FLblLineInGain <> nil then FLblLineInGain.Visible := LineIn;
if FEdLineInGain <> nil then FEdLineInGain.Visible := LineIn;
end;
procedure TSettingsForm.SetTXMicBackend(HasHWMic: Boolean);
// HasHWMic — Caps.HasHWMic: микрофонный разъём самого трансивера. Всё, что
// стоит в строке разъёма, уезжает в DUC Specific байт 50 (openHPSDR); у AD936x
// такого разъёма нет, звук идёт только с ПК.
begin
if FTXHasHWMic = HasHWMic then Exit;
FTXHasHWMic := HasHWMic;
RelayoutTXProfileSub;
end;
procedure TSettingsForm.RelayoutTXProfileSub;
// Подвкладка Profile: без микрофонного разъёма группа Microphone теряет целую
// строку, Mic gain поднимается в неё, а три группы ниже подтягиваются — иначе
// на месте снятой строки осталась бы дыра.
const
TX_MARGIN = 22;
TX_STEP = 42;
TX_R1 = 42;
TX_MIC_H = 134;
TX_FILT_H = 108;
TX_DYN_H = 220;
TX_PWR_H = 148;
var
MicH, GainY, Y: Integer;
procedure Vis(C: TControl; V: Boolean);
begin
if C <> nil then C.Visible := V;
end;
begin
if (FTXGrpMic = nil) or (FTXGrpFilt = nil) or (FTXGrpDyn = nil)
or (FTXGrpPwr = nil) or (FTXSubPage[0] = nil) then Exit;
if not FPageTransmit.Visible then begin FTXRelayoutDue := True; Exit; end;
FTXRelayoutDue := False;
// ★Как и на вкладке Calibration: TScrollBox двигает детей при прокрутке, и
// SetBounds ниже задаёт координаты ОТНОСИТЕЛЬНО прокрученного клиента.
// Перекладка случается при смене трансивера на живой программе, то есть
// страницу к этому моменту уже успели пролистать, — и без сброса группы
// разъезжались: всё уезжало вверх, а Microphone оставался на месте.
FPageTransmit.VertScrollBar.Position := 0;
FPageTransmit.HorzScrollBar.Position := 0;
Vis(FLblMicJack, FTXHasHWMic);
Vis(FCmbTXMicJack, FTXHasHWMic);
Vis(FChkTXMicBias, FTXHasHWMic);
Vis(FChkTXMicPTT, FTXHasHWMic);
if not FTXHasHWMic then
begin
// Пара «Boost ↔ Line In gain» обычно переключается UpdateMicJackVisibility;
// здесь снимаем обе, а тот выше уже знает, что возвращать их не должен.
Vis(FChkTXMicBoost, False);
Vis(FLblLineInGain, False);
Vis(FEdLineInGain, False);
Vis(FLblTipRing, False);
Vis(FCmbTXMicTR, False);
end
else
UpdateMicJackVisibility; // вернуть ту из пары, что положена по разъёму
if FTXHasHWMic then
begin
GainY := TX_R1 + TX_STEP;
MicH := TX_MIC_H;
end
else
begin
GainY := TX_R1;
MicH := TX_MIC_H - TX_STEP;
end;
if FLblMicGain <> nil then FLblMicGain.Top := DpiScale(GainY + 5);
if FEdTXMicGain <> nil then FEdTXMicGain.Top := DpiScale(GainY);
// Ширину групп ведёт RelayoutResponsiveCards — здесь только высота и Top.
FTXGrpMic.SetBounds(DpiScale(TX_MARGIN), 0, FTXGrpMic.Width, DpiScale(MicH));
Y := MicH + 12;
FTXGrpFilt.Top := DpiScale(Y);
Inc(Y, TX_FILT_H + 12);
FTXGrpDyn.Top := DpiScale(Y);
Inc(Y, TX_DYN_H + 12);
FTXGrpPwr.Top := DpiScale(Y);
FTXSubPage[0].Height := DpiScale(Y + TX_PWR_H + 12);
UpdatePageBottomSpace(FPageTransmit);
if FActivePage = FPageTransmit then UpdateSettingsScrollBar;
end;
procedure TSettingsForm.SetWidebandBackend(HasWideband: Boolean);
// Кадры широкополосного АЦП приходят только от openHPSDR. У AD936x галка
// открывала бы пустую панель, поэтому её здесь нет вовсе.
begin
if FChkShowWideband <> nil then FChkShowWideband.Visible := HasWideband;
if FChkWidebandFill <> nil then FChkWidebandFill.Visible := HasWideband;
end;
procedure TSettingsForm.SetADCBackend(HasDitherRandom: Boolean);
// Dither/random — свойства АЦП openHPSDR; у AD936x такого понятия нет.
begin
if FAdvHasADC = HasDitherRandom then Exit; // не сбрасывать прокрутку зря
FAdvHasADC := HasDitherRandom;
RelayoutAdvancedTab;
end;
procedure TSettingsForm.RelayoutAdvancedTab;
// Без dither/random группа ADC пуста — прячем её целиком, а Web Server
// поднимается на её место, иначе страница начиналась бы с пустоты.
const
ADV_ADC_H = 128;
begin
if (FAdvGrpADC = nil) or (FAdvGrpWeb = nil) or (FLblAdvHint = nil) then Exit;
if not FPageAdvanced.Visible then begin FAdvRelayoutDue := True; Exit; end;
FAdvRelayoutDue := False;
FPageAdvanced.VertScrollBar.Position := 0;
FPageAdvanced.HorzScrollBar.Position := 0;
if FAdvHasADC then
FLblAdvHint.Caption := 'ADC processing and built-in web server settings.'
else
FLblAdvHint.Caption := 'Built-in web server settings.';
FAdvGrpADC.Visible := FAdvHasADC;
if FAdvHasADC then
FAdvGrpWeb.Top := DpiScale(86 + ADV_ADC_H + 16)
else
FAdvGrpWeb.Top := DpiScale(86);
UpdatePageBottomSpace(FPageAdvanced);
if FActivePage = FPageAdvanced then UpdateSettingsScrollBar;
end;
procedure TSettingsForm.OnMicJackChange(Sender: TObject);
begin
UpdateMicJackVisibility;
@@ -2716,8 +2928,11 @@ begin
if FEdLineInGain <> nil then FEdLineInGain.Value := EnsureRange(T.LineInGainDB, -34.5, 12.0);
if FChkTXMicBias <> nil then FChkTXMicBias.Checked := T.MicBias;
if FChkTXMicPTT <> nil then FChkTXMicPTT.Checked := T.MicPTTEnabled;
// Tip/Ring (Orion boards 4 и 5)
IsOrion := BoardType in [4, 5];
// Tip/Ring (Orion boards 4 и 5). ★Без микрофонного разъёма строку уже снял
// RelayoutTXProfileSub, и вернуть её отсюда значило бы вернуть дыру: этот
// Load зовётся при каждом открытии окна, а SetTXMicBackend — только на
// смене железа.
IsOrion := FTXHasHWMic and (BoardType in [4, 5]);
if FLblTipRing <> nil then FLblTipRing.Visible := IsOrion;
if FCmbTXMicTR <> nil then
begin
@@ -2862,13 +3077,20 @@ var
Spin: TFlatSpinEdit;
i, r, c, X, Y: Integer;
begin
MakePageHeader(FPagePA, 'Power Amplifier',
'Output limit and per-band calibration values.');
// Подпись под заголовком зависит от железа (RelayoutPATab): у AD936x на
// странице остаётся один потолок аттенюации, и обещать «per-band calibration»
// там нечестно. Поэтому создаём её отдельно, а не через ASubtitle.
MakePageHeader(FPagePA, 'Power Amplifier', '');
FLblPAHint := MakeTextBlock(FPagePA, '', SETTINGS_MARGIN, 48,
SETTINGS_CARD_W, 20);
FLblPAHint.Tag := TAG_PAGE_HEADER;
Grp := MakeGroupPanel(FPagePA, 'Power && Calibration', MARGIN, 86, SETTINGS_CARD_W, GRP_H);
FPAGrpPower := Grp;
// Max Power
Lbl := MakeLbl(Grp, 'Max power (W)', GRP_PAD, R1 + 6, 132);
FLblPAMaxPower := Lbl;
Spin := TFlatSpinEdit.Create(Self);
Spin.Parent := Grp;
@@ -2884,8 +3106,9 @@ begin
FEdMaxPower := Spin;
// Pluto TX max attenuation @ drive 100% (потолок под внешний усилитель), дБ<0
Lbl := MakeLbl(Grp, 'Pluto TX att @100% (dB)',
Lbl := MakeLbl(Grp, 'TX att @100% drive (dB)',
GRP_PAD + 320, R1 + 6, 180);
FLblPlutoTxAtt := Lbl;
FEdPlutoTxMaxAtt := TFlatFloatSpinEdit.Create(Self);
FEdPlutoTxMaxAtt.Parent := Grp;
@@ -2903,6 +3126,7 @@ begin
// Calibration label
Lbl := MakeLbl(Grp, 'Calibration per band, 38.8 ... 100.0',
GRP_PAD, R1 + ROW_H + 12, 320);
FLblPABandHdr := Lbl;
// Band grid 4 columns × 3 rows
for i := 0 to 10 do
@@ -2913,6 +3137,7 @@ begin
Y := R1 + ROW_H + 34 + r * ROW_H;
Lbl := MakeLbl(Grp, PA_BAND_NAMES[i] + ':', X, Y + 5, LBL_W);
FPABandLabels[i] := Lbl;
FEdBandCal[i] := MakeFloatSpin(Grp, X + LBL_W, Y, OnBandCalChange);
FEdBandCal[i].Tag := i;
@@ -2921,6 +3146,7 @@ begin
// ── VHF Calibration (Transverters) ──────────────────────────────────────
Grp := MakeGroupPanel(FPagePA, 'VHF Calibration (Transverters)',
MARGIN, 86 + GRP_H + 14, SETTINGS_CARD_W, R1 + 4 * ROW_H + 44);
FPAGrpVHF := Grp;
Lbl := MakeLbl(Grp, 'Calibration per transverter slot, 38.8 ... 100.0',
GRP_PAD, R1, 420);
@@ -2946,6 +3172,95 @@ begin
FEdVHFCal[i].Tag := i;
end;
FPAHasPA := True; // до подключения показываем полный (openHPSDR) вид
RelayoutPATab;
end;
procedure TSettingsForm.SetPABackend(HasPA: Boolean);
// HasPA — Caps.HasPA активного бэкенда: мост детектора мощности, drive-байт и
// телеметрия питания. У AD936x ничего этого нет.
begin
if FPAHasPA = HasPA then Exit;
FPAHasPA := HasPA;
RelayoutPATab;
end;
procedure TSettingsForm.RelayoutPATab;
// openHPSDR: потолок мощности + калибровка драйва по диапазонам и слотам
// трансвертера (всё это кормит CalcDriveByte). AD936x: мощность задаётся
// железной аттенюацией, drive-байта нет вовсе — на странице остаётся один
// потолок аттенюации, остальное прячется вместе со своей группой.
const
PA_MARGIN = 22;
PA_R1 = 42;
PA_ROW_H = 32;
PA_GRP_H = 222;
PA_GRP_PAD = 18;
PA_VHF_H = PA_R1 + 4 * PA_ROW_H + 44;
var
i, H: Integer;
Lbl: TLabel;
begin
if (FPAGrpPower = nil) or (FPAGrpVHF = nil) or (FLblPAHint = nil) then Exit;
if not FPagePA.Visible then begin FPARelayoutDue := True; Exit; end;
FPARelayoutDue := False;
// Как и на вкладке Calibration: SetBounds ниже задаёт координаты относительно
// прокрученного клиента, поэтому сперва возвращаем страницу в ноль.
FPagePA.VertScrollBar.Position := 0;
FPagePA.HorzScrollBar.Position := 0;
if FPAHasPA then
FLblPAHint.Caption := 'Output limit and per-band calibration values.'
else
FLblPAHint.Caption := 'Transmit power is set by hardware attenuation; '
+ 'there is no power detector to calibrate.';
// Заголовок группы — по её содержимому: калибровки у AD936x не остаётся.
Lbl := GroupTitle(FPAGrpPower);
if Lbl <> nil then
if FPAHasPA then Lbl.Caption := 'Power && Calibration'
else Lbl.Caption := 'Transmit power';
if FLblPAMaxPower <> nil then FLblPAMaxPower.Visible := FPAHasPA;
if FEdMaxPower <> nil then FEdMaxPower.Visible := FPAHasPA;
if FLblPABandHdr <> nil then FLblPABandHdr.Visible := FPAHasPA;
for i := 0 to High(FPABandLabels) do
begin
if FPABandLabels[i] <> nil then FPABandLabels[i].Visible := FPAHasPA;
if FEdBandCal[i] <> nil then FEdBandCal[i].Visible := FPAHasPA;
end;
FPAGrpVHF.Visible := FPAHasPA;
// Аттенюация — свойство AD936x. У openHPSDR мощность идёт drive-байтом, и
// поле было бы мёртвым; у AD936x оно единственное и переезжает в первую
// колонку, чтобы не висеть посреди пустой группы.
if (FLblPlutoTxAtt <> nil) and (FEdPlutoTxMaxAtt <> nil) then
begin
FLblPlutoTxAtt.Visible := not FPAHasPA;
FEdPlutoTxMaxAtt.Visible := not FPAHasPA;
if FPAHasPA then
begin
FLblPlutoTxAtt.Left := DpiScale(PA_GRP_PAD + 320);
FEdPlutoTxMaxAtt.Left := DpiScale(PA_GRP_PAD + 510);
end
else
begin
FLblPlutoTxAtt.Left := DpiScale(PA_GRP_PAD);
FEdPlutoTxMaxAtt.Left := DpiScale(PA_GRP_PAD + 190);
end;
end;
if FPAHasPA then H := PA_GRP_H
else H := PA_R1 + BTN_H + 2 + 18; // ровно одна строка
// Ширину не трогаем: карточки тянет по окну RelayoutResponsiveCards, и
// фиксированная ширина здесь схлопнула бы их до базовой.
FPAGrpPower.SetBounds(DpiScale(PA_MARGIN), DpiScale(86),
FPAGrpPower.Width, DpiScale(H));
if FPAHasPA then
FPAGrpVHF.SetBounds(DpiScale(PA_MARGIN), DpiScale(86 + H + 14),
FPAGrpVHF.Width, DpiScale(PA_VHF_H));
UpdatePageBottomSpace(FPagePA);
if FActivePage = FPagePA then UpdateSettingsScrollBar;
end;
procedure TSettingsForm.BuildCalibrationTab;
@@ -3004,8 +3319,12 @@ begin
FCalXvtrName[i] := '';
end;
MakePageHeader(FPageCalib, 'Calibration',
'Power/SWR detector, supply voltage/current and S-meter calibration.');
// Подпись зависит от железа (RelayoutCalTab): у AD936x детектора мощности и
// телеметрии питания нет, и обещать их калибровку в подзаголовке нечестно.
MakePageHeader(FPageCalib, 'Calibration', '');
FLblCalHint := MakeTextBlock(FPageCalib, '', SETTINGS_MARGIN, 48,
SETTINGS_CARD_W, 20);
FLblCalHint.Tag := TAG_PAGE_HEADER;
// ── Power / SWR detector ────────────────────────────────────────────────
GTop := 86;
@@ -3177,6 +3496,8 @@ begin
if (FCalGrpDet = nil) or (FCalGrpVA = nil) or (FCalGrpSM = nil)
or (FCalGrpDisp = nil) or (FLblDispXvtrHdr = nil)
or (FCalGrpFreq = nil) then Exit;
if not FPageCalib.Visible then begin FCalRelayoutDue := True; Exit; end;
FCalRelayoutDue := False;
// TScrollBox двигает детей при прокрутке, и SetBounds ниже задаёт координаты
// ОТНОСИТЕЛЬНО прокрученного клиента. Если страницу успели прокрутить (у
@@ -3186,6 +3507,14 @@ begin
FPageCalib.VertScrollBar.Position := 0;
FPageCalib.HorzScrollBar.Position := 0;
if FLblCalHint <> nil then
if FCalIsPluto then
FLblCalHint.Caption := 'S-meter, spectrum and reference frequency '
+ 'calibration.'
else
FLblCalHint.Caption := 'Power/SWR detector, supply voltage/current and '
+ 'S-meter calibration.';
// ---- подписи/видимость строк Det. Cal. (своя сетка, уже с подсказкой) ----
for i := 0 to CFG_BAND_COUNT - 1 do
begin
@@ -3410,11 +3739,15 @@ var
Lbl: TLabel;
Y, i: Integer;
begin
MakePageHeader(FPageAdvanced, 'Advanced',
'ADC processing and built-in web server settings.');
MakePageHeader(FPageAdvanced, 'Advanced', '');
FLblAdvHint := MakeTextBlock(FPageAdvanced, '', SETTINGS_MARGIN, 48,
SETTINGS_CARD_W, 20);
FLblAdvHint.Tag := TAG_PAGE_HEADER;
// ── ADC ───────────────────────────────────────────────────────────────────
Grp := MakeGroupPanel(FPageAdvanced, 'ADC', MARGIN, 86, SETTINGS_CARD_W, 128);
FAdvGrpADC := Grp;
FAdvHasADC := True; // до подключения показываем полный (openHPSDR) вид
Chk := TFlatCheckBox.Create(Self);
Chk.Parent := Grp;
@@ -3438,6 +3771,7 @@ begin
// ── Web Server ────────────────────────────────────────────────────────────
Grp := MakeGroupPanel(FPageAdvanced, 'Web Server', MARGIN, 230, SETTINGS_CARD_W, 318);
FAdvGrpWeb := Grp;
Y := R1;
Chk := TFlatCheckBox.Create(Self);
@@ -3515,6 +3849,8 @@ begin
// TCI Server живёт на вкладке CAT (см. BuildCATTab): это такой же канал
// внешнего управления трансивером, что и CAT, и оператор ищет его там.
RelayoutAdvancedTab; // стартовая раскладка (полный, openHPSDR-вид)
end;
procedure TSettingsForm.BuildDXClusterTab;
@@ -6110,6 +6446,20 @@ begin
end;
end;
procedure TSettingsForm.SetQOBackend(IsPluto: Boolean);
// Транспондер QO-100 у нас существует только на AD936x: InQO100 в контроллере
// требует IsPluto (полный дуплекс с раздельными RX/TX LO). На openHPSDR вся
// вкладка мертва, поэтому её нет в списке.
var
Changed: Boolean;
begin
Changed := FNavQO.Visible <> IsPluto;
FNavQO.Visible := IsPluto;
if Changed then RelayoutNav; // иначе на её месте останется дыра
if (not IsPluto) and FPageQO.Visible then
SelectPage(FPageAudio, FNavAudio);
end;
procedure TSettingsForm.SetOCBackend(IsPluto: Boolean);
// У Pluto/AD936x нет Open Collector выхода — вкладка целиком скрывается,
// а не показывает пустое содержимое (в отличие от Antenna, где Pluto имеет
+6
View File
@@ -230,6 +230,8 @@ type
procedure SetLastSWR(V: Double);
function GetPAMaxPower: Double;
procedure SetPAMaxPower(V: Double);
function GetHasPATelemetry: Boolean;
procedure SetHasPATelemetry(V: Boolean);
function GetTransmitting: Boolean;
procedure SetTransmitting(V: Boolean);
procedure SetPbSMeterRight(V: TPaintBox);
@@ -301,6 +303,8 @@ type
property LastFwdW: Double read GetLastFwdW write SetLastFwdW;
property LastSWR: Double read GetLastSWR write SetLastSWR;
property PAMaxPower: Double read GetPAMaxPower write SetPAMaxPower;
// Есть ли мост детектора мощности: без него шкала PWR/SWR рисуется пустой.
property HasPATelemetry: Boolean read GetHasPATelemetry write SetHasPATelemetry;
property Transmitting: Boolean read GetTransmitting write SetTransmitting;
// ── Флаги обновления ──────────────────────────────────────────────────────
@@ -612,6 +616,8 @@ function TSpectrumView.GetLastSWR: Double; begin Result := FSMeter.Las
procedure TSpectrumView.SetLastSWR(V: Double); begin FSMeter.LastSWR := V; end;
function TSpectrumView.GetPAMaxPower: Double; begin Result := FSMeter.PAMaxPower; end;
procedure TSpectrumView.SetPAMaxPower(V: Double); begin FSMeter.PAMaxPower := V; end;
function TSpectrumView.GetHasPATelemetry: Boolean; begin Result := FSMeter.HasPATelemetry; end;
procedure TSpectrumView.SetHasPATelemetry(V: Boolean); begin FSMeter.HasPATelemetry := V; end;
function TSpectrumView.GetTransmitting: Boolean; begin Result := FSMeter.Transmitting; end;
procedure TSpectrumView.SetTransmitting(V: Boolean); begin FSMeter.Transmitting := V; end;
procedure TSpectrumView.SetPbSMeterRight(V: TPaintBox); begin FSMeter.PbSMeterRight := V; end;