mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user