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
+152
View File
@@ -80,6 +80,7 @@ type
rfFMDeviation, rfFMCTCSS, rfFMCTCSSTone, rfFMSQ, rfFMSQLevel,
rfFMStep, rfFMStepIdx, rfFMRpt,
rfChannel,
rfTXProfile, // сменился активный TX-профиль
rfSMeter, rfPower, rfSWR, rfSupply, rfPLL, // телеметрия
rfADCOverload, // ADC overload (рендер индикатора)
rfBeaconLock, // QO-100 beacon lock: статус/коррекция
@@ -424,6 +425,10 @@ type
// ---- Конфиги / устройство (per-MAC) ----
FTXSettings: TTXSettings;
// Именованные TX-профили («звуковая» часть FTXSettings + drive, см.
// TTXProfile). Истина для эфира — всегда живые FTXSettings/FDrivePercent;
// активный профиль ведётся за ними (StoreActiveTXProfile).
FTXProfiles: TTXProfileList;
FAlexSettings: TAlexSettings;
FOCSettings: TOCSettings; // OC Control (Open Collector, openHPSDR-only)
// Антенные разъёмы AD936x (Pluto/LibreSDR) на диапазон/трансвертер.
@@ -577,6 +582,21 @@ type
procedure ApplyTXSettingsToDSP; // TX-цепь (фильтр/gain/EQ/comp/...) → WDSP
function PSOutlierSigmaEff: Double; // σ outlier-фильтра calcc (авто по плате)
// ---- TX-профили --------------------------------------------------------
// Профиль — именованный снимок «звуковой» части TX-цепи (состав в
// TTXProfile). Отдельной кнопки «сохранить» нет: правки ручками пишутся в
// активный профиль сразу (StoreActiveTXProfile), как и всё остальное в
// EWSDR. Переключение применяет снимок в эфир немедленно.
function TXProfileCount: Integer;
function TXProfileName(Idx: Integer): string;
function ActiveTXProfile: Integer;
procedure SelectTXProfile(Idx: Integer);
procedure StoreActiveTXProfile(Persist: Boolean = False);
function AddTXProfile(const AName: string): Integer; // форк текущего звука
procedure RenameTXProfile(Idx: Integer; const AName: string);
procedure DeleteTXProfile(Idx: Integer);
procedure SaveTXProfilesToConfig;
// ---- Управление диапазонами (состояние + DSP; рендер через события) ----
// MakeBandSettings — снимок текущего состояния в запись диапазона.
// SaveCurrentBand — сохраняет текущее состояние в кэш+конфиг (или в XVTR-слот).
@@ -4222,6 +4242,10 @@ procedure TRadioController.SetDrive(V: Integer);
begin
if V < 0 then V := 0 else if V > 100 then V := 100;
FDrivePercent := V;
// Мощность входит в профиль: ведём её в активном без записи на диск (слайдер
// шлёт события пачками; на диск профили уходят на переключении/остановке).
if (FTXProfiles.ActiveIdx >= 0) and (FTXProfiles.ActiveIdx < FTXProfiles.Count) then
FTXProfiles.Items[FTXProfiles.ActiveIdx].DrivePercent := FDrivePercent;
FDriveLevel := CalcDriveByte;
ApplyWDSPDrive(FDrivePercent);
ApplyPlutoTxAtten; // Pluto: drive живьём меняет TX-аттенюацию во время передачи
@@ -4904,6 +4928,124 @@ begin
FNetwork.SendDUCSpecific(DUCPkt);
end;
// ---------------------------------------------------------------------------
// TX-профили
// ---------------------------------------------------------------------------
function TRadioController.TXProfileCount: Integer;
begin
Result := FTXProfiles.Count;
end;
function TRadioController.TXProfileName(Idx: Integer): string;
begin
if (Idx < 0) or (Idx >= FTXProfiles.Count) then Result := ''
else Result := FTXProfiles.Items[Idx].Name;
end;
function TRadioController.ActiveTXProfile: Integer;
begin
Result := FTXProfiles.ActiveIdx;
end;
procedure TRadioController.SaveTXProfilesToConfig;
begin
if (not FDevConnected) or (not Assigned(FSettings)) then Exit;
FSettings.SaveTXProfiles(FDevMAC, FTXProfiles);
FSettings.Save;
end;
procedure TRadioController.StoreActiveTXProfile(Persist: Boolean = False);
// Живое состояние → активный профиль. Зовётся на каждой правке TX-настроек и
// на смене drive; Persist=False не трогает диск (drive ездит слайдером).
var Idx: Integer;
begin
Idx := FTXProfiles.ActiveIdx;
if (Idx < 0) or (Idx >= FTXProfiles.Count) then Exit;
TXProfileCapture(FTXSettings, FDrivePercent, FTXProfiles.Items[Idx]);
if Persist then SaveTXProfilesToConfig;
end;
procedure TRadioController.SelectTXProfile(Idx: Integer);
var
OldMic: Byte;
NewDrive: Integer;
begin
if (Idx < 0) or (Idx >= FTXProfiles.Count) then Exit;
// Уходящий профиль дописываем живыми значениями — ручные правки не теряются.
StoreActiveTXProfile;
OldMic := BuildMicLineSelectByte;
NewDrive := FDrivePercent;
FTXProfiles.ActiveIdx := Idx;
TXProfileApply(FTXProfiles.Items[Idx], FTXSettings, NewDrive);
ApplyTXSettingsToDSP;
// Маршрутизация микрофона живёт в железе (DUC Specific byte 50) — при смене
// Mic In/Line In/boost/bias пакет надо перепослать, WDSP тут ни при чём.
if BuildMicLineSelectByte <> OldMic then SendDUCSpecificFromSettings;
// SetDrive пересчитает калиброванный байт, толкнёт WDSP/Pluto и HP-кадр;
// зовём безусловно — при активном TUN уровень берётся из TUNLevel профиля.
SetDrive(NewDrive);
if FTuning and FWDSPReady then
FDSPEngine.SetDriveLevel(EnsureRange(FTXSettings.TUNLevel, 0, 100) / 100.0);
if FDevConnected and Assigned(FSettings) then
begin
FSettings.SaveTX(FDevMAC, FTXSettings);
FSettings.SaveTXProfiles(FDevMAC, FTXProfiles);
FSettings.Save;
end;
Changed(rfTXProfile);
end;
function TRadioController.AddTXProfile(const AName: string): Integer;
// Форк: новый профиль = текущий звук под новым именем, он же становится активным.
begin
Result := -1;
if FTXProfiles.Count >= TXPROF_MAX then Exit;
Result := FTXProfiles.Count;
TXProfileCapture(FTXSettings, FDrivePercent, FTXProfiles.Items[Result]);
FTXProfiles.Items[Result].Name := Copy(Trim(AName), 1, 31);
if FTXProfiles.Items[Result].Name = '' then
FTXProfiles.Items[Result].Name := 'Profile ' + IntToStr(Result + 1);
Inc(FTXProfiles.Count);
FTXProfiles.ActiveIdx := Result;
SaveTXProfilesToConfig;
Changed(rfTXProfile);
end;
procedure TRadioController.RenameTXProfile(Idx: Integer; const AName: string);
var S: string;
begin
if (Idx < 0) or (Idx >= FTXProfiles.Count) then Exit;
S := Copy(Trim(AName), 1, 31);
if S = '' then Exit;
FTXProfiles.Items[Idx].Name := S;
SaveTXProfilesToConfig;
Changed(rfTXProfile);
end;
procedure TRadioController.DeleteTXProfile(Idx: Integer);
// Последний профиль не удаляем: список без активного элемента сломал бы
// авто-сохранение (StoreActiveTXProfile писать некуда).
var i: Integer;
begin
if (Idx < 0) or (Idx >= FTXProfiles.Count) or (FTXProfiles.Count <= 1) then Exit;
for i := Idx to FTXProfiles.Count - 2 do
FTXProfiles.Items[i] := FTXProfiles.Items[i + 1];
Dec(FTXProfiles.Count);
if FTXProfiles.ActiveIdx > Idx then Dec(FTXProfiles.ActiveIdx)
else if FTXProfiles.ActiveIdx = Idx then
begin
// Удалили активный: встаём на соседний и применяем его в эфир. ActiveIdx
// гасим — иначе StoreActiveTXProfile внутри Select затрёт соседа живым
// звуком удалённого профиля.
FTXProfiles.ActiveIdx := -1;
SelectTXProfile(EnsureRange(Idx, 0, FTXProfiles.Count - 1));
Exit; // SelectTXProfile уже сохранил и отрендерил
end;
SaveTXProfilesToConfig;
Changed(rfTXProfile);
end;
procedure TRadioController.ApplyTXSettingsToDSP;
begin
if not FWDSPReady then Exit;
@@ -5847,6 +5989,12 @@ begin
// drive% (0..100) — раньше грузился через TrkDrive.Position; теперь напрямую,
// слайдер выставит rfDevice-рендер. Калиброванный байт считаем после FCurrentBand.
FDrivePercent := EnsureRange(FLoadedGlobal.DriveLevel, 0, 100);
// TX-профили. Секции нет (конфиг до этой версии) — строим заводской набор из
// ТЕКУЩИХ настроек: «Default» повторяет их один в один, звук после
// обновления не меняется. Активный профиль на старте не применяем — живая
// секция "tx" уже хранит последнее состояние, оно и есть истина.
if not FSettings.LoadTXProfiles(FDevMAC, FTXProfiles) then
DefaultTXProfiles(FTXSettings, FDrivePercent, FTXProfiles);
// Клампим LastBand — иначе битый/старый конфиг → RestoreBand выйдет по гарду,
// а последующие SetMode/SetFilterIdx запишут в FBandCache[невалид] за массив.
FCurrentBand := EnsureRange(FLoadedGlobal.LastBand, 0, CFG_BAND_COUNT - 1);
@@ -5999,6 +6147,10 @@ begin
FSettings.SaveAnt936x(FDevMAC, FAnt936x);
FSettings.SaveOC(FDevMAC, FOCSettings);
FSettings.SaveFilters(FDevMAC, FFilterTable);
// Активный TX-профиль дописываем живыми значениями (drive/громкость микро-
// фона ездят мимо диалога настроек) и пишем список целиком.
StoreActiveTXProfile;
FSettings.SaveTXProfiles(FDevMAC, FTXProfiles);
// XVTR LastFreq → актуальная видимая частота в момент остановки.
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;