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
+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;