feat(filter): произвольные кромки фильтра — таблица по режиму, редактор, drag, TX-полоска

Фильтр главного приёмника перестал быть одним числом. Теперь это таблица
(Name, Low, High) на режим: 10 пресетов + VAR1/VAR2, знаковые кромки (у LSB
своя строка, у USB своя), паритет с Thetis FilterForm. Слайсы независимые
кромки умели и раньше — главный приёмник настраивался только пресетами.

Модель (Settings/RadioController):
- TFilterSlot/TFilterSet/TFilterTable + заводские дефолты из прежних FILT_*_BW
  и FILT_*_NAMES (переехали в Settings — из них строится таблица).
- FFilterLo/FFilterHi = истина, FFilterBW производная (FilterBWFromEdges),
  поэтому все прежние читатели ширины (web, зум, band-cache, CAT) не тронуты.
- ApplyFilterSlot — единственная точка «слот → состояние тракта».
- SetFilterEdges: произвольные кромки уходят в VAR1 с автопереключением,
  пресеты нельзя испортить случайным движением мыши или энкодера.
- Персист: секция "filters" под MAC, пишутся только режимы, отличные от
  заводских; секция режима сносится целиком, иначе возврат слота к
  заводскому оставлял бы старый ключ.
- Диапазон помнит только выбранный слот: кромки живут в таблице, второго
  источника истины у VAR нет.

UI:
- 12 кнопок фильтра (2 ряда по 6), подписи из живой таблицы.
- FilterPopup — поповер редактора по ПКМ на кнопке фильтра, по образцу
  PureSignalPopup (дочерний контрол формы, не top-level: Wayland). Слева
  слоты, справа Name/Low/High/Width, внизу форма полосы на знаковой оси.
- Перетаскивание края полосы на спектре: ±5px, снап 10 Гц, Alt = симметрично
  (не Ctrl — он создаёт слайс). Отказ на передаче и когда полоса на экране
  уже 12px, иначе захват съедал бы клик-тюн.

Отрисовка:
- Полоса главного VFO рисуется по РЕАЛЬНЫМ кромкам приёмника, слайса — по его
  кромкам; вывод из режима и ширины остался фолбэком. Пара→ширина→пара было
  преобразованием с потерями: несимметричный фильтр из слайс-CAT (ZZFL/ZZFH)
  рисовался не там, где звучал.
- На передаче полоса считается по TX-фильтру (TXSignedEdges), а не по
  приёмной ширине — TX-полоса от RX не зависит (модель Thetis).
- CalcFilterBandXFor больше не держит свою копию таблицы знака боковой:
  единственная таблица теперь FilterEdgesFromBW.

Управление:
- CAT ZZFL/ZZFH заработали на основном порту (были заглушками).
- Андромеда: энкодеры #4/#5 — независимые Filter High/Low, как на железе G2
  (было: #4 ширина, #5 не реализован).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 13:15:33 +03:00
co-authored by Claude Opus 5
parent a820182716
commit 25521a2d10
13 changed files with 1329 additions and 308 deletions
+176 -136
View File
@@ -215,6 +215,11 @@ type
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
// Единственная точка, где выбранный слот превращается в состояние тракта
// (FFilter/кромки/производная BW/FM-девиация). Тракт не трогает — только state.
procedure ApplyFilterSlot(Idx: Integer);
function ClampFilterIdx(Mode, Idx: Integer): Integer;
procedure CacheFilterToBand; // фильтр текущего банда в band-cache
function ActiveVfoHz: Double; // частота активного VFO (A или B)
function AnchoredPan(AZoom: Double): Double; // пан, центрирующий окно на маркере VFO
procedure ApplyTuneCore(VfoHz: Double); // CTUN shift/center/scroll для актив. VFO
@@ -275,8 +280,14 @@ type
FVfoA, FVfoB: Double;
FActiveVfo: Integer;
FMode: Integer;
FFilter: Integer;
FFilter: Integer; // слот 0..FILT_SLOTS-1 (10/11 = VAR1/VAR2)
// Знаковые кромки текущего фильтра — ИСТИНА. FFilterBW производная
// (FilterBWFromEdges) и оставлена ради прежних читателей (web, зум,
// band-cache, CAT); писать в неё напрямую нельзя.
FFilterLo: Integer;
FFilterHi: Integer;
FFilterBW: Integer;
FFilterTable: TFilterTable; // редактируемые фильтры по режиму
FAGCMode: Integer;
FAGCTop: Integer;
FRxGainMode: Integer; // Pluto hw-gain: 0=manual,1=fast,2=slow,3=hybrid
@@ -670,6 +681,23 @@ type
procedure SetSliceFilter(Id, Low, High: Integer);
procedure SetSliceModeBW(Id, Mode, BW: Integer); // режим+полоса → кромки фильтра
class procedure FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
// --- Редактируемая таблица фильтров (пресеты + VAR1/VAR2 на режим) ---
function FilterSlot(Mode, Idx: Integer): TFilterSlot;
procedure SetFilterSlot(Mode, Idx: Integer; const S: TFilterSlot);
procedure ResetFilterSlot(Mode, Idx: Integer);
procedure ResetFilterSet(Mode: Integer);
// Произвольные кромки (drag на спектре, CAT ZZFL/ZZFH, web): пишутся в
// VAR1 и переключают на него, если текущий слот — пресет. Пресеты таким
// образом невозможно испортить случайным движением мыши.
procedure SetFilterEdges(LoHz, HiHz: Integer);
// Только state, без применения к тракту: UpdateFilterButtons — рендер,
// DSP в его путях перестраивается отдельно (как было до таблицы).
procedure SelectFilterSlotState(Idx: Integer);
procedure SanitizeFilterState;
// Знаковые края TX-полосы (для отрисовки полосы фильтра при передаче).
procedure TXFilterEdges(out Lo, Hi: Integer);
property FilterLo: Integer read FFilterLo;
property FilterHi: Integer read FFilterHi;
class function AGCModeFromUI(M: Integer): TWDSPAGCMode; // UI-индекс 0..4 → WDSP enum
class function AGCModeToUI(A: TWDSPAGCMode): Integer; // и обратно
procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
@@ -1037,7 +1065,9 @@ begin
FDeviceStore := TDeviceStore.Create; // общий список устройств (desktop+web)
// Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны).
FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0;
FMode := MODE_USB; FFilter := 5; FFilterBW := 2700;
FMode := MODE_USB; FFilter := 5;
DefaultFilterTable(FFilterTable); // до подключения — заводские фильтры
ApplyFilterSlot(FFilter);
FAGCMode := 1; FAGCTop := 90;
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
@@ -3161,18 +3191,10 @@ end;
// Кромки полосы под режим/полосу (единый расчёт для главного канала и слайсов).
class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
var Half: Integer;
// Обёртка над единственной таблицей знака (Settings.FilterEdgesFromBW) —
// оставлена ради прежних вызывающих (MainForm, SetSliceModeBW).
begin
Half := BW div 2;
case Mode of
MODE_LSB: begin Lo := -BW; Hi := -100; end;
MODE_USB: begin Lo := 100; Hi := BW; end;
// DIGI: полоса от нуля — поднесущие цифровых видов начинаются низко,
// срез на 100 Гц как в SSB съел бы их.
MODE_DIGL: begin Lo := -BW; Hi := 0; end;
MODE_DIGU: begin Lo := 0; Hi := BW; end;
else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM/WFM
end;
FilterEdgesFromBW(Mode, BW, Lo, Hi);
end;
procedure TRadioController.SetSliceModeBW(Id, Mode, BW: Integer);
@@ -3189,7 +3211,10 @@ procedure TRadioController.ApplyModeFilter;
var
Lo, Hi: Integer;
begin
FilterEdgesFor(FMode, FFilterBW, Lo, Hi);
// Кромки берём как есть: они уже знаковые и уже под текущий режим (таблица
// фильтров ведётся ПО режиму), конвертировать боковую больше нечем и негде.
Lo := FFilterLo;
Hi := FFilterHi;
if FWDSPReady and Assigned(FDSPEngine) then
begin
// Только RX. TX-полоса НЕ следует за RX-фильтром (модель Thetis) — её
@@ -3216,61 +3241,53 @@ end;
// Выбирает дефолтный фильтр (индекс + полоса + FM-девиация) под текущий FMode.
// Зеркалит state-часть UI UpdateFilterButtons, чтобы смена режима была
// корректной и без UI (headless). FM сохраняет текущий NFM/WFM, прочие — дефолт.
function TRadioController.ClampFilterIdx(Mode, Idx: Integer): Integer;
// VAR1/VAR2 есть у любого режима, поэтому проверяем только пресетную часть.
// Выход за таблицу режима возвращает ЗАВОДСКОЙ индекс (а не крайний) — так
// вело себя прежнее «if FFilter > FILT_WFM_COUNT-1 then FILT_WFM_DEF».
begin
Result := Idx;
if (Idx = FILT_VAR1) or (Idx = FILT_VAR2) then Exit;
if (Idx < 0) or (Idx > FilterPresetCount(Mode) - 1) then
Result := FilterDefIdx(Mode);
end;
procedure TRadioController.ApplyFilterSlot(Idx: Integer);
var S: TFilterSlot;
begin
FFilter := ClampFilterIdx(FMode, Idx);
S := FFilterTable[FMode][FFilter];
FFilterLo := S.Lo;
FFilterHi := S.Hi;
FFilterBW := FilterBWFromEdges(FMode, S.Lo, S.Hi);
// FM-девиация привязана к пресету (NFM/WFM), у VAR берём девиацию дефолта.
if FMode = MODE_FM then
begin
if FFilter <= 1 then FFMDeviation := FILT_FM_DEV[FFilter]
else FFMDeviation := FILT_FM_DEV[FILT_FM_DEF];
end
else if FMode = MODE_DMR then
FFMDeviation := DMR_FM_DEV;
end;
procedure TRadioController.ApplyModeDefaults;
begin
// FM/WFM/DMR/FMRAW сохраняют выбранный пресет (NFM↔WFM и т.п.), остальные
// режимы возвращаются к заводскому индексу — как было до таблицы фильтров.
case FMode of
MODE_FM:
begin
if FFilter > 1 then FFilter := FILT_FM_DEF;
FFilterBW := FILT_FM_BW[FFilter];
FFMDeviation := FILT_FM_DEV[FFilter];
end;
MODE_WFM:
begin
if FFilter > FILT_WFM_COUNT - 1 then FFilter := FILT_WFM_DEF;
FFilterBW := FILT_WFM_BW[FFilter];
end;
MODE_DMR:
begin
FFilter := 0;
FFilterBW := FILT_DMR_BW;
FFMDeviation := DMR_FM_DEV;
end;
MODE_FMRAW:
begin
if FFilter > FILT_RAW_COUNT - 1 then FFilter := FILT_RAW_DEF;
FFilterBW := FILT_RAW_BW[FFilter];
end;
MODE_LSB, MODE_USB:
begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
MODE_DIGU, MODE_DIGL:
begin FFilter := FILT_DIGI_DEF; FFilterBW := FILT_DIGI_BW[FFilter]; end;
MODE_DSB:
begin FFilter := FILT_DSB_DEF; FFilterBW := FILT_DSB_BW[FFilter]; end;
MODE_CWL, MODE_CWU:
begin FFilter := FILT_CW_DEF; FFilterBW := FILT_CW_BW[FFilter]; end;
else begin FFilter := FILT_AM_DEF; FFilterBW := FILT_AM_BW[FFilter]; end;
MODE_FM, MODE_WFM, MODE_DMR, MODE_FMRAW: ApplyFilterSlot(FFilter);
else ApplyFilterSlot(FilterDefIdx(FMode));
end;
end;
function TRadioController.FilterBWFor(M, Idx: Integer): Integer;
// Полоса выбранного фильтра под режим. Зеркалит выбор таблицы в UpdateFilterButtons,
// но НЕ меняет индекс (в отличие от ApplyModeDefaults) — нужно для XVTR-restore.
var S: TFilterSlot;
begin
case M of
MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, FILT_FM_COUNT - 1)];
MODE_WFM: Result := FILT_WFM_BW[EnsureRange(Idx, 0, FILT_WFM_COUNT - 1)];
MODE_DMR: Result := FILT_DMR_BW;
MODE_FMRAW: Result := FILT_RAW_BW[EnsureRange(Idx, 0, FILT_RAW_COUNT - 1)];
MODE_LSB, MODE_USB:
Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
MODE_DIGU, MODE_DIGL:
Result := FILT_DIGI_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
MODE_DSB: Result := FILT_DSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
MODE_CWL, MODE_CWU:
Result := FILT_CW_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
else Result := FILT_AM_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
end;
M := EnsureRange(M, 0, CFG_MODE_MAX);
S := FFilterTable[M][ClampFilterIdx(M, Idx)];
Result := FilterBWFromEdges(M, S.Lo, S.Hi);
end;
function TRadioController.FilterIdxFor(M, BW: Integer): Integer;
@@ -3346,26 +3363,10 @@ begin
end;
procedure TRadioController.SanitizeFilterForMode;
// Для FM индексы фильтров только 0/1 (NFM/WFM); приводим FFilter и
// синхронизируем FFilterBW/FFMDeviation. Чистая (state) часть UpdateFilterButtons.
// У FM/DMR/FMRAW пресетов меньше, чем у SSB: приводим индекс к допустимому и
// синхронизируем кромки/BW/девиацию. Чистая (state) часть UpdateFilterButtons.
begin
if FMode = MODE_FM then
begin
if FFilter > 1 then FFilter := FILT_FM_DEF;
FFilterBW := FILT_FM_BW[FFilter];
FFMDeviation := FILT_FM_DEV[FFilter];
end;
if FMode = MODE_DMR then
begin
FFilter := 0;
FFilterBW := FILT_DMR_BW;
FFMDeviation := DMR_FM_DEV;
end;
if FMode = MODE_FMRAW then
begin
if FFilter > FILT_RAW_COUNT - 1 then FFilter := FILT_RAW_DEF;
FFilterBW := FILT_RAW_BW[FFilter];
end;
if FMode in [MODE_FM, MODE_DMR, MODE_FMRAW] then ApplyFilterSlot(FFilter);
end;
function TRadioController.MakeBandSettings: TBandSettings;
@@ -3544,9 +3545,10 @@ begin
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
FFilter := B.FilterIdx;
FFilterBW := B.FilterBW;
SanitizeFilterForMode;
// Банд помнит ТОЛЬКО выбранный слот; кромки живут в таблице фильтров
// (по режиму, как в Thetis). Иначе у VAR было бы два источника истины —
// ровно та болезнь, от которой уходили.
ApplyFilterSlot(B.FilterIdx);
ApplyModeFilter;
// --- AGC (единый маппинг — исправляет каст-баг RestoreBand) ---
@@ -3985,72 +3987,107 @@ end;
procedure TRadioController.SetFilterIdx(Idx: Integer);
begin
// Клампим индекс под текущий режим (как FilterBWFor) — публичная команда,
// Idx может прийти из CAT/web без проверки; FM/WFM-таблицы короче остальных.
case FMode of
MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1);
MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1);
MODE_DMR: Idx := 0;
MODE_FMRAW: Idx := EnsureRange(Idx, 0, FILT_RAW_COUNT - 1);
else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1);
// Публичная команда: Idx может прийти из CAT/web без проверки. Клампится
// внутри ApplyFilterSlot (у FM/WFM/FMRAW пресетов меньше; VAR всегда есть).
ApplyFilterSlot(Idx);
CacheFilterToBand;
ApplyModeFilter;
Changed(rfFilter);
end;
procedure TRadioController.CacheFilterToBand;
begin
FBandCache[FCurrentBand].FilterIdx := FFilter;
FBandCache[FCurrentBand].FilterBW := FFilterBW; // легаси-поле, для старых конфигов
end;
procedure TRadioController.SelectFilterSlotState(Idx: Integer);
begin
ApplyFilterSlot(Idx);
end;
procedure TRadioController.SanitizeFilterState;
begin
SanitizeFilterForMode;
end;
procedure TRadioController.TXFilterEdges(out Lo, Hi: Integer);
begin
Lo := 0; Hi := 0;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.TXFilterEdgesHz(Lo, Hi);
end;
function TRadioController.FilterSlot(Mode, Idx: Integer): TFilterSlot;
begin
Result := FFilterTable[EnsureRange(Mode, 0, CFG_MODE_MAX)]
[EnsureRange(Idx, 0, FILT_SLOTS - 1)];
end;
procedure TRadioController.SetFilterSlot(Mode, Idx: Integer; const S: TFilterSlot);
begin
Mode := EnsureRange(Mode, 0, CFG_MODE_MAX);
Idx := EnsureRange(Idx, 0, FILT_SLOTS - 1);
if S.Hi <= S.Lo then Exit; // пустая полоса = глухой приём
FFilterTable[Mode][Idx] := S;
if Assigned(FSettings) then FSettings.SaveFilters(FDevMAC, FFilterTable);
// Правка активного слота слышна сразу — форма настроек в EWSDR мгновенная.
if (Mode = FMode) and (Idx = FFilter) then
begin
ApplyFilterSlot(Idx);
CacheFilterToBand;
ApplyModeFilter;
Changed(rfFilter);
end;
FFilter := Idx;
case FMode of
MODE_LSB, MODE_USB: FFilterBW := FILT_SSB_BW[Idx];
MODE_DIGU, MODE_DIGL: FFilterBW := FILT_DIGI_BW[Idx];
MODE_DSB: FFilterBW := FILT_DSB_BW[Idx];
MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx];
MODE_WFM: FFilterBW := FILT_WFM_BW[Idx];
MODE_DMR: FFilterBW := FILT_DMR_BW;
MODE_FMRAW: FFilterBW := FILT_RAW_BW[Idx];
MODE_FM:
begin
FFilterBW := FILT_FM_BW[Idx];
FFMDeviation := FILT_FM_DEV[Idx];
end;
else FFilterBW := FILT_AM_BW[Idx];
end;
FBandCache[FCurrentBand].FilterBW := FFilterBW;
end;
procedure TRadioController.ResetFilterSlot(Mode, Idx: Integer);
var Def: TFilterSet;
begin
Mode := EnsureRange(Mode, 0, CFG_MODE_MAX);
Def := DefaultFilterSet(Mode);
SetFilterSlot(Mode, Idx, Def[EnsureRange(Idx, 0, FILT_SLOTS - 1)]);
end;
procedure TRadioController.ResetFilterSet(Mode: Integer);
var i: Integer;
begin
for i := 0 to FILT_SLOTS - 1 do ResetFilterSlot(Mode, i);
end;
procedure TRadioController.SetFilterEdges(LoHz, HiHz: Integer);
// Drag на спектре / CAT ZZFL-ZZFH / web: пресеты не портим — уходим в VAR1.
var S: TFilterSlot; Target: Integer;
begin
if HiHz - LoHz < FILT_MIN_WIDTH then Exit;
if (FFilter = FILT_VAR1) or (FFilter = FILT_VAR2) then Target := FFilter
else Target := FILT_VAR1;
S := FFilterTable[FMode][Target];
S.Lo := LoHz;
S.Hi := HiHz;
FFilterTable[FMode][Target] := S;
if Assigned(FSettings) then FSettings.SaveFilters(FDevMAC, FFilterTable);
ApplyFilterSlot(Target);
CacheFilterToBand;
ApplyModeFilter;
Changed(rfFilter);
end;
procedure TRadioController.SetFilterBW(BWHz: Integer);
// Непрерывная полоса (web-ползунок): применяем запрошенный BW как есть, а
// FFilter подбираем ближайшим пресетом — только для подсветки кнопки.
// Непрерывная полоса (web-ползунок). Точное совпадение с пресетом выбирает
// пресет, произвольная ширина уходит в VAR — как и перетаскивание края мышью.
var
BW_arr: array[0..FILT_COUNT-1] of Integer;
best, dist, i: Integer;
Lo, Hi, i, n: Integer;
begin
if FMode = MODE_DMR then BWHz := FILT_DMR_BW;
FFilterBW := BWHz;
for i := 0 to FILT_COUNT-1 do BW_arr[i] := MaxInt;
case FMode of
MODE_LSB, MODE_USB:
for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i];
MODE_DIGU, MODE_DIGL:
for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DIGI_BW[i];
MODE_DSB: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DSB_BW[i];
MODE_CWL, MODE_CWU:
for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i];
MODE_FM: for i := 0 to FILT_FM_COUNT-1 do BW_arr[i] := FILT_FM_BW[i];
MODE_WFM: for i := 0 to FILT_WFM_COUNT-1 do BW_arr[i] := FILT_WFM_BW[i];
MODE_DMR: BW_arr[0] := FILT_DMR_BW;
MODE_FMRAW: for i := 0 to FILT_RAW_COUNT-1 do BW_arr[i] := FILT_RAW_BW[i];
else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
end;
best := 0; dist := MaxInt;
for i := 0 to FILT_COUNT-1 do
if Abs(BW_arr[i] - BWHz) < dist then
FilterEdgesFromBW(FMode, BWHz, Lo, Hi);
n := FilterPresetCount(FMode);
for i := 0 to n - 1 do
if (FFilterTable[FMode][i].Lo = Lo) and (FFilterTable[FMode][i].Hi = Hi) then
begin
dist := Abs(BW_arr[i] - BWHz);
best := i;
SetFilterIdx(i);
Exit;
end;
FFilter := best;
if FMode = MODE_FM then FFMDeviation := FILT_FM_DEV[best];
FBandCache[FCurrentBand].FilterBW := FFilterBW;
ApplyModeFilter;
Changed(rfFilter);
SetFilterEdges(Lo, Hi);
end;
procedure TRadioController.SetRawFMDeviation(Hz: Integer);
@@ -5788,6 +5825,8 @@ begin
FNetwork.SetAlexConfig(FAlexSettings);
FSettings.LoadOC(FDevMAC, FOCSettings);
FNetwork.SetOCConfig(FOCSettings); // no-op на Pluto (base-класс)
FSettings.LoadFilters(FDevMAC, FFilterTable);
ApplyFilterSlot(FFilter); // кромки под таблицу этого устройства
FSettings.LoadXvtr(FDevMAC, FXvtrSettings);
FCurrentXvtr := -1;
ApplyXvtrToNetwork;
@@ -5959,6 +5998,7 @@ begin
FSettings.SaveAlex(FDevMAC, FAlexSettings);
FSettings.SaveAnt936x(FDevMAC, FAnt936x);
FSettings.SaveOC(FDevMAC, FOCSettings);
FSettings.SaveFilters(FDevMAC, FFilterTable);
// XVTR LastFreq → актуальная видимая частота в момент остановки.
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;