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