feat(tx): TX-профили + вкладка Transmit на подвкладках

Профиль — именованный снимок «как я звучу», переключаемый одним кликом
(идиома FlexRadio, состав по таблице TXProfile у Thetis).

Граница «профиль ↔ устройство»:
- в профиль: микрофонный вход (jack/boost/bias/PTT/Tip-Ring/Line-in gain),
  MicGain, кромки TX-фильтра, компрессор/leveler/ALC/phase rotator, EQ,
  AM carrier, Drive и Tune level;
- у устройства остаются ATT on TX, FIR/фаза/окно фильтра (это задержка
  тракта, а не тембр), TUNFreq, FM/CTCSS, TX Display, TX Grid и все
  параметры PureSignal — смена профиля не должна перерисовывать спектр
  и ломать калибровку PS.

Кнопки «сохранить» нет: активный профиль ведётся за живым состоянием
(StoreActiveTXProfile из OnTXSettingsChange и SetDrive) — как и все
остальные настройки в EWSDR, применяемые мгновенно.

Settings.pas: TTXProfile/TTXProfileList, TXProfileCapture/TXProfileApply
(единственные две точки, знающие состав профиля), DefaultTXProfiles
(Default/SSB DX/SSB Wide/ESSB 3.5k/ESSB 5k — наследуют mic-вход и
мощность от текущих настроек), Load/SaveTXProfiles (секция tx_profiles
под MAC). Секции нет — заводской набор строится из текущих настроек,
поэтому миграция старого конфига не меняет звук.

RadioController: FTXProfiles, rfTXProfile, SelectTXProfile (снимок → DSP
+ перепосыл DUC Specific при смене mic-байта + SetDrive + персист),
Add/Rename/Delete (последний профиль не удаляется).

MainForm: кнопка профиля в свободных колонках ряда DUP/RX MUTE — блок TX
не вырос по высоте; ЛКМ — список, ПКМ — Settings→Transmit→Profile.

SettingsForm: страница Transmit разбита на подвкладки Profile / EQ /
Hardware / Display + полоса профиля сверху; свиток 2000 px вместо этого
раскладывается на четыре коротких экрана.

Проверено: round-trip персиста отдельным тестом (все поля, активный
индекс, усечение списка) и offscreen-снимки всех четырёх подвкладок.
На железе не проверено.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 14:21:09 +03:00
co-authored by Claude Opus 5
parent 24890ca8a7
commit 71a97b63f0
4 changed files with 940 additions and 71 deletions
+148 -1
View File
@@ -356,6 +356,10 @@ type
TrkAtt: TFlatSlider;
BtnChannels: TFlatButton;
FChannelsDropDown: TFlatDropDown;
// TX-профиль: кнопка с именем активного (блок TX, ряд DUP) + список.
// ЛКМ — список, ПКМ — вкладка Transmit в настройках.
BtnTXProfile: TFlatButton;
FTXProfileDropDown: TFlatDropDown;
FChannelsForm: TObject; // TChannelsForm (cast при использовании)
FBeaconScopeForm: TBeaconScopeForm; // окно констелляции маяка (ПКМ по BEACON)
@@ -517,6 +521,17 @@ type
Shift: TShiftState; X, Y: Integer);
procedure OnChannelsDropDownSelect(Sender: TObject; Idx: Integer);
procedure RefreshChannelsDropDown;
// TX-профили: рендер кнопки/списка + обработчики блока TX и вкладки Transmit.
procedure RenderTXProfile;
procedure PushTXProfilesToSettings;
procedure BtnTXProfileClick(Sender: TObject);
procedure BtnTXProfileMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure OnTXProfileDropDownSelect(Sender: TObject; Idx: Integer);
procedure OnTXProfileSelectFromSettings(Idx: Integer);
procedure OnTXProfileAddFromSettings(const AName: string);
procedure OnTXProfileRenameFromSettings(Idx: Integer; const AName: string);
procedure OnTXProfileDeleteFromSettings(Idx: Integer);
procedure OnChannelStoreChanged(Sender: TObject);
procedure OnChannelActiveChanged(Idx: Integer; const Name: string; Active: Boolean);
procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
@@ -1245,6 +1260,9 @@ begin
FController.FNetwork.OnWideband := OnWidebandCB;
// Дефолты TX, Alex и XVTR (на случай если устройство ещё не выбрано).
TSettingsManager.DefaultTX(FController.FTXSettings);
// Заводской набор TX-профилей до подключения (ConnectDevice перечитает свой).
DefaultTXProfiles(FController.FTXSettings, FController.FDrivePercent,
FController.FTXProfiles);
TSettingsManager.DefaultAlex(FController.FAlexSettings);
TSettingsManager.DefaultOC(FController.FOCSettings);
TSettingsManager.DefaultXvtr(FController.FXvtrSettings);
@@ -1582,7 +1600,12 @@ begin
TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive%
UpdateDriveLabel; // % / дБ рядом с ползунком
FChannelController.OnDriveChanged; // внешнее изменение снимает память канала
// Мощность — часть TX-профиля: держим подпись в настройках свежей.
if (FSettingsForm <> nil) and TSettingsForm(FSettingsForm).Visible then
TSettingsForm(FSettingsForm).SetTXProfileDrive(FController.FDrivePercent);
end;
rfTXProfile:
RenderTXProfile;
rfAGCTop:
begin
TrkAGC.Position := FController.FAGCTop;
@@ -1739,6 +1762,7 @@ begin
FWidebandView.SetFillSpectrum(FController.FWidebandFill);
FSpecView.FillSpectrum := FController.FSpectrumFill;
if BtnDUP <> nil then StyleButton(BtnDUP, FController.FDisplayDuplex);
RenderTXProfile; // список профилей этого устройства (загружен по MAC)
if FController.FLoadedGlobal.DisplayFPS > 0 then
ApplyFPS(FController.FLoadedGlobal.DisplayFPS);
ApplyFreqMhzDigits(EnsureRange(FController.FLoadedGlobal.FreqMhzDigits, 3, 5));
@@ -2093,6 +2117,21 @@ begin
StyleButton(BtnRxMute, FController.FRxMuteOnTx);
UpdateRxMuteButton; // видимость/активность по DUP
// TX-профиль — свободные колонки 2..3 того же ряда (панель не растёт по
// высоте). Подпись = имя активного профиля, ЛКМ — список, ПКМ — настройки.
BtnTXProfile := MakeBtn(PanelTX, 'Default',
LeftPanelButtonLeft(LEFT_W, 4, 2), 44,
LeftPanelButtonLeft(LEFT_W, 4, 3)
+ LeftPanelButtonWidth(LEFT_W, 4, 3)
- LeftPanelButtonLeft(LEFT_W, 4, 2),
BTN_SM, BtnTXProfileClick);
BtnTXProfile.OnMouseDown := BtnTXProfileMouseDown;
StyleButton(BtnTXProfile, False);
FTXProfileDropDown := TFlatDropDown.Create(BtnTXProfile, PanelLeft, 1, BTN_SM);
FTXProfileDropDown.OnSelect := OnTXProfileDropDownSelect;
RenderTXProfile;
LblDrv := TLabel.Create(Self);
LblDrv.Parent := PanelTX; LblDrv.Left := 4; LblDrv.Top := 77;
LblDrv.Caption := 'DRV'; LblDrv.Font.Color := CLR_TEXTDIM;
@@ -3188,6 +3227,8 @@ begin
end;
if BtnChannels <> nil then StyleButton(BtnChannels, False);
if FChannelsDropDown <> nil then FChannelsDropDown.ApplyStyle(StyleButton, T.Panel);
if BtnTXProfile <> nil then StyleButton(BtnTXProfile, False);
if FTXProfileDropDown <> nil then FTXProfileDropDown.ApplyStyle(StyleButton, T.Panel);
StyleButton(BtnNR, BtnNR.Active);
StyleButton(BtnNB, BtnNB.Active);
StyleButton(BtnSNB, BtnSNB.Active);
@@ -5486,7 +5527,8 @@ procedure TMainForm.CloseCTCSSPopup;
begin
if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup;
CloseFMStepPopup;
if FChannelsDropDown <> nil then FChannelsDropDown.ClosePopup;
if FChannelsDropDown <> nil then FChannelsDropDown.ClosePopup;
if FTXProfileDropDown <> nil then FTXProfileDropDown.ClosePopup;
end;
procedure TMainForm.BtnFMRptMinusClick(Sender: TObject);
@@ -5592,6 +5634,7 @@ begin
if FController.FChannelStore.Count = 0 then Exit;
FCTCSSDropDown.ClosePopup;
FStepDropDown.ClosePopup;
if FTXProfileDropDown <> nil then FTXProfileDropDown.ClosePopup;
FChannelsDropDown.Toggle;
end;
@@ -5627,6 +5670,101 @@ begin
FChannelController.Select(Idx);
end;
// ---------------------------------------------------------------------------
// TX-профили (кнопка блока TX + список)
// ---------------------------------------------------------------------------
procedure TMainForm.RenderTXProfile;
// Единая точка рендера: подпись кнопки, состав списка и — если открыт диалог
// настроек — его вкладка Transmit (профиль переписал живые TX-настройки).
var
Names: array of string;
i, Act, Cnt: Integer;
begin
if BtnTXProfile = nil then Exit;
Cnt := FController.TXProfileCount;
Act := FController.ActiveTXProfile;
if (Act >= 0) and (Act < Cnt) then
BtnTXProfile.Caption := FController.TXProfileName(Act)
else
BtnTXProfile.Caption := 'TX Profile';
BtnTXProfile.Invalidate;
if FTXProfileDropDown <> nil then
begin
SetLength(Names, Cnt);
for i := 0 to Cnt - 1 do Names[i] := FController.TXProfileName(i);
FTXProfileDropDown.SetItems(Names);
if (Act >= 0) and (Act < Cnt) then FTXProfileDropDown.SetItemIndex(Act);
end;
if (FSettingsForm <> nil) and TSettingsForm(FSettingsForm).Visible then
begin
TSettingsForm(FSettingsForm).LoadTXSettings(FController.FTXSettings,
IfThen(FController.FNetwork.Connected,
FController.FNetwork.Device.BoardType, FPendingBoardType));
PushTXProfilesToSettings;
end;
end;
procedure TMainForm.PushTXProfilesToSettings;
var
Names: array of string;
i: Integer;
begin
if FSettingsForm = nil then Exit;
SetLength(Names, FController.TXProfileCount);
for i := 0 to FController.TXProfileCount - 1 do
Names[i] := FController.TXProfileName(i);
TSettingsForm(FSettingsForm).LoadTXProfiles(Names, FController.ActiveTXProfile,
FController.FDrivePercent);
end;
procedure TMainForm.BtnTXProfileClick(Sender: TObject);
begin
if FController.TXProfileCount = 0 then Exit;
// Чужие списки гасим поимённо: общий CloseCTCSSPopup закрыл бы и наш, и
// повторный клик по кнопке никогда не сворачивал бы список.
if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup;
CloseFMStepPopup;
FTXProfileDropDown.Toggle;
end;
procedure TMainForm.BtnTXProfileMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if Button <> mbRight then Exit;
// ПКМ — прямо в редактор профиля (вкладка Transmit), как ПКМ по кнопке фильтра.
if FTXProfileDropDown <> nil then FTXProfileDropDown.ClosePopup;
BtnSettingsClick(nil);
if FSettingsForm <> nil then TSettingsForm(FSettingsForm).ShowTransmitPage;
end;
procedure TMainForm.OnTXProfileDropDownSelect(Sender: TObject; Idx: Integer);
begin
// Применение (WDSP + mic-байт + drive) и персист — в контроллере;
// рендер придёт назад событием rfTXProfile.
FController.SelectTXProfile(Idx);
end;
procedure TMainForm.OnTXProfileSelectFromSettings(Idx: Integer);
begin
FController.SelectTXProfile(Idx);
end;
procedure TMainForm.OnTXProfileAddFromSettings(const AName: string);
begin
FController.AddTXProfile(AName);
end;
procedure TMainForm.OnTXProfileRenameFromSettings(Idx: Integer; const AName: string);
begin
FController.RenameTXProfile(Idx, AName);
end;
procedure TMainForm.OnTXProfileDeleteFromSettings(Idx: Integer);
begin
FController.DeleteTXProfile(Idx);
end;
procedure TMainForm.OnChannelActiveChanged(Idx: Integer; const Name: string; Active: Boolean);
begin
// Рендер кнопки Channel по событию адаптера каналов (Active=True → подсветка+имя).
@@ -7812,10 +7950,14 @@ begin
end;
end;
end;
// Правка «звуковых» полей — это правка активного профиля: отдельной кнопки
// «сохранить» нет, профиль ведётся за живым состоянием (см. TTXProfile).
FController.StoreActiveTXProfile;
// Сохраняем сразу — настройки per-device, попадают в hpsdr_settings.json
if FController.FDevConnected then
begin
FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings);
FController.FSettings.SaveTXProfiles(FController.FDevMAC, FController.FTXProfiles);
FController.FSettings.Save;
end;
end;
@@ -8624,6 +8766,10 @@ begin
SF.OnThemeChange := SetLightTheme;
SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits;
SF.OnTXChange := OnTXSettingsChange;
SF.OnTXProfileSelect := OnTXProfileSelectFromSettings;
SF.OnTXProfileAdd := OnTXProfileAddFromSettings;
SF.OnTXProfileRename := OnTXProfileRenameFromSettings;
SF.OnTXProfileDelete := OnTXProfileDeleteFromSettings;
SF.OnAlexChange := OnAlexSettingsChange;
SF.OnXvtrChange := OnXvtrSettingsChange;
SF.OnOCChange := OnOCSettingsChange;
@@ -8632,6 +8778,7 @@ begin
SF.OnWebSettingsChange := ApplyWebSettings;
end;
SF := TSettingsForm(FSettingsForm);
PushTXProfilesToSettings;
SF.LoadTXSettings(FController.FTXSettings,
IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType));
SF.LoadAlexSettings(FController.FAlexSettings,