Phase 3 (batch 15): move band save/restore into the controller

Per the architecture decision, band state lifecycle belongs on the
controller (it operates entirely on controller state + engines;
persistence is already delegated to Settings.pas). A separate unit would
need friend access to ~20 fields + engines — worse coupling.

- MakeBandSettings, SaveCurrentBand: moved verbatim (pure over controller
  state; SaveCurrentBand handles the XVTR-slot vs HF band-cache split and
  delegates the write to FSettings). MainForm.SaveCurrentBand is now a thin
  delegate (22 call sites unchanged); MakeBandSettings removed from MainForm.
- RestoreBand: state + DSP application moved to the controller; rendering is
  driven by per-field Changed events (rfBand/rfMode/rfFilter/rfAGC*/rfFM*/
  rfCTun/rfVfoB). The FM filter-index sanitize (which UpdateFilterButtons
  did inline as a render side-effect) is extracted into a pure
  SanitizeFilterForMode so the DSP ApplyModeFilter still runs on the
  sanitized BW (preserves original ordering). Fixes the same AGC index-cast
  bug as earlier batches (now ApplyAGCToEngine).
- MainForm.RestoreBand is a thin wrapper: ResetWfAvgBuf (band-switch-only,
  not per-tune) + controller.RestoreBand + ApplyVfoA(FVfoA) for the final
  tune with channel orchestration (auto-CTCSS / pre-channel drive).
- Added rfFMStep render case (BtnFMStep on/off was styled ad-hoc).

Builds clean, links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Uladzimir Karpenka
2026-06-04 21:37:02 +03:00
co-authored by Claude Opus 4.8
parent a8a192a3a3
commit 24102343ea
2 changed files with 170 additions and 135 deletions
+15 -135
View File
@@ -412,7 +412,6 @@ type
procedure RestoreWindowBounds; procedure RestoreWindowBounds;
procedure SaveWindowBounds; procedure SaveWindowBounds;
function MakeGlobalSettings: TGlobalSettings; function MakeGlobalSettings: TGlobalSettings;
function MakeBandSettings: TBandSettings;
procedure FreqDispBChanged(Sender: TObject; NewFreq: Int64); procedure FreqDispBChanged(Sender: TObject; NewFreq: Int64);
procedure BtnBandClick(Sender: TObject); procedure BtnBandClick(Sender: TObject);
procedure BtnModeClick(Sender: TObject); procedure BtnModeClick(Sender: TObject);
@@ -787,28 +786,6 @@ begin
FController.FSettings.SaveWindowBounds(L, T, Wd, Ht, DPI, Maximized); FController.FSettings.SaveWindowBounds(L, T, Wd, Ht, DPI, Maximized);
end; end;
function TMainForm.MakeBandSettings: TBandSettings;
begin
Result.VfoA := FController.FVfoA;
Result.VfoB := FController.FVfoB;
Result.Mode := FController.FMode;
Result.FilterIdx := FController.FFilter;
Result.FilterBW := FController.FFilterBW;
Result.AGCMode := FController.FAGCMode;
Result.AGCTop := FController.FAGCTop;
Result.CTun := FController.FCTun;
Result.SpanHz := FController.FSpanHz;
Result.FMSQOn := FController.FFMSQOn;
Result.FMSQLevel := FController.FFMSQLevel;
Result.CTCSSOn := FController.FFMCTCSSOn;
Result.CTCSSToneIdx := FController.FFMCTCSSToneIdx;
Result.FMStepOn := FController.FFMStepOn;
Result.FMStepIdx := FController.FFMStepIdx;
Result.FMRptDir := FController.FFMRptDir;
Result.FMRptOffsetHz := FController.FFMRptOffsetHz;
// Waterfall AGC/NF — глобальные (не диапазонные)
end;
function TMainForm.MakeGlobalSettings: TGlobalSettings; function TMainForm.MakeGlobalSettings: TGlobalSettings;
var i: Integer; var i: Integer;
begin begin
@@ -889,124 +866,21 @@ end;
procedure TMainForm.SaveCurrentBand; procedure TMainForm.SaveCurrentBand;
begin begin
if not FController.FDevConnected then Exit; // Логика сохранения (band-cache/XVTR-слот + конфиг) — в контроллере.
// В XVTR-режиме НЕ трогаем HF band-cache: FController.FBandCache[FController.FCurrentBand] FController.SaveCurrentBand;
// относится к HF-диапазону, на котором пользователь был ДО активации
// трансвертера, и FController.FVfoA сейчас на XVTR-частоте (144.x, 432.x и т.д.).
// Запись в FController.FBandCache[FController.FCurrentBand] стерла бы корректное HF-состояние.
// Вместо этого сохраняем LastFreq в XVTR-настройках.
if FController.FCurrentXvtr >= 0 then
begin
if (FController.FCurrentXvtr < CFG_XVTR_COUNT) then
begin
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFreq := FController.FVfoA;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastMode := FController.FMode;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFilterIdx := FController.FFilter;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMSQOn := FController.FFMSQOn;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMSQLevel := FController.FFMSQLevel;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastCTCSSOn := FController.FFMCTCSSOn;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastCTCSSToneIdx := FController.FFMCTCSSToneIdx;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMStepOn := FController.FFMStepOn;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMStepIdx := FController.FFMStepIdx;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMRptDir := FController.FFMRptDir;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMRptOffsetHz := FController.FFMRptOffsetHz;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastCTun := FController.FCTun;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastAGCMode := FController.FAGCMode;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastAGCTop := FController.FAGCTop;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastCTCSSAutoActive := FController.FFMCTCSSAutoActive;
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMRptAutoActive := FController.FFMRptAutoActive;
end;
FController.FSettings.SaveXvtr(FController.FDevMAC, FController.FXvtrSettings);
Exit;
end;
// Не сохраняем, если VFO ушёл в VHF/UHF область (например, после применения
// канала на 2м без XVTR) — иначе HF-кэш диапазона будет перезаписан VHF-частотой.
if FController.FVfoA > 61000000 then Exit;
FController.FBandCache[FController.FCurrentBand] := MakeBandSettings;
FController.FSettings.SaveBand(FController.FDevMAC, FController.FCurrentBand, FController.FBandCache[FController.FCurrentBand]);
end; end;
procedure TMainForm.RestoreBand(BandIdx: Integer); procedure TMainForm.RestoreBand(BandIdx: Integer);
var
B: TBandSettings;
i: Integer;
begin begin
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit; if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
B := FController.FBandCache[BandIdx]; // Сброс порогов водопада — только при явной смене диапазона (не на каждом
// пересечении границы при тюнинге, поэтому здесь, а не в rfBand-рендере).
// --- Кнопки диапазонов ---
for i := 0 to BAND_COUNT - 1 do
StyleButton(BtnBand[i], i = BandIdx);
// Сбрасываем waterfall thresholds при смене диапазона
FSpecView.ResetWfAvgBuf; FSpecView.ResetWfAvgBuf;
// Состояние + DSP + события рендера — в контроллере.
// --- Режим: сначала устанавливаем FController.FMode, потом кнопки и WDSP --- FController.RestoreBand(BandIdx);
FController.FMode := B.Mode; // Финальная перестройка VFO A с канальной оркестрацией (auto-CTCSS,
for i := 0 to MODE_COUNT - 1 do // pre-channel drive) — через UI-путь ApplyVfoA.
StyleButton(BtnMode[i], i = FController.FMode); ApplyVfoA(Round(FController.FVfoA));
if FController.FWDSPReady then
FController.FDSPEngine.SetMode(FController.FMode);
// --- Фильтр ---
FController.FFilter := B.FilterIdx;
FController.FFilterBW := B.FilterBW;
UpdateFilterButtons; // обновляет кнопки фильтра
FController.ApplyModeFilter; // применяет Lo/Hi в WDSP с учётом нового FController.FMode
// --- AGC ---
FController.FAGCMode := B.AGCMode;
FController.FAGCTop := B.AGCTop;
for i := 0 to 4 do
StyleButton(BtnAGCMode[i], i = FController.FAGCMode);
TrkAGC.Position := FController.FAGCTop;
LblAGCTop.Caption := Format('%ddB', [FController.FAGCTop]);
if FController.FWDSPReady then
begin
FController.FDSPEngine.SetAGCTop(FController.FAGCTop);
FController.FDSPEngine.SetAGC(TWDSPAGCMode(FController.FAGCMode), 50.0);
end;
FSpecView.AGCTop := FController.FAGCTop;
// --- FM Squelch + CTCSS ---
FController.FFMSQOn := B.FMSQOn;
FController.FFMSQLevel := B.FMSQLevel;
// UI update happens inside UpdateFilterButtons (called above) when mode=FM
FController.FFMCTCSSOn := B.CTCSSOn;
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, FController.FFMCTCSSOn);
SetFMCTCSSTone(B.CTCSSToneIdx);
ApplyFMSquelch;
FController.FFMStepOn := B.FMStepOn;
SetFMStep(B.FMStepIdx);
if BtnFMStep <> nil then StyleButton(BtnFMStep, FController.FFMStepOn);
FController.FFMRptDir := B.FMRptDir;
FController.FFMRptOffsetHz := B.FMRptOffsetHz;
if FController.FFMRptOffsetHz <= 0 then
FController.FFMRptOffsetHz := RptDefaultOffsetHz(B.VfoA);
if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, FController.FFMRptDir = RPT_MINUS);
if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, FController.FFMRptDir = RPT_PLUS);
if EdFMRptOffset <> nil then EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz);
// --- CTUN ---
FController.FCTun := B.CTun;
StyleButton(BtnCTun, FController.FCTun);
if FController.FWDSPReady and not FController.FCTun then
FController.FDSPEngine.SetShift(0.0);
// --- Sample Rate / Span ---
// SampleRate — глобальный, не per-band. Не восстанавливаем из диапазона.
FController.FSpanHz := FController.FSampleRate;
if Assigned(FSampleRateOverlay) then
FSampleRateOverlay.SetCurrentRate(FController.FSampleRate);
// --- VFO A: центрируем на новой частоте, сбрасываем shift ---
FController.FCenterFreq := B.VfoA; // DDC = центр диапазона
FController.FVfoB := B.VfoB;
FreqDispB.Frequency := Round(FController.FVfoB);
// Пересчитываем drive byte для нового диапазона перед отправкой в сеть
FController.FDriveLevel := CalcDriveByte;
// ApplyVfoA обновит сеть, FreqDisp, shift, Ruler — перерисует.
ApplyVfoA(Round(B.VfoA));
end; end;
procedure TMainForm.SaveAllAndExit; procedure TMainForm.SaveAllAndExit;
@@ -1492,6 +1366,12 @@ begin
if (FController.FMode = MODE_FM) and (FSpecView <> nil) then if (FController.FMode = MODE_FM) and (FSpecView <> nil) then
FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx]; FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx];
end; end;
rfFMStep:
if BtnFMStep <> nil then
begin
StyleButton(BtnFMStep, FController.FFMStepOn);
BtnFMStep.Repaint;
end;
rfFMSQ: rfFMSQ:
begin begin
StyleButton(BtnFMSQ, FController.FFMSQOn); StyleButton(BtnFMSQ, FController.FFMSQOn);
+155
View File
@@ -110,6 +110,7 @@ type
procedure Changed(Field: TRadioField); procedure Changed(Field: TRadioField);
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг) procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
function ActiveVfoHz: Double; // частота активного VFO (A или B) function ActiveVfoHz: Double; // частота активного VFO (A или B)
procedure ApplyTuneCore(VfoHz: Double); // CTUN shift/center/scroll для актив. VFO procedure ApplyTuneCore(VfoHz: Double); // CTUN shift/center/scroll для актив. VFO
public public
@@ -222,6 +223,15 @@ type
// (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием. // (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием.
function CalcDriveByte: Byte; function CalcDriveByte: Byte;
// ---- Управление диапазонами (состояние + DSP; рендер через события) ----
// MakeBandSettings — снимок текущего состояния в запись диапазона.
// SaveCurrentBand — сохраняет текущее состояние в кэш+конфиг (или в XVTR-слот).
// RestoreBand — загружает состояние диапазона, применяет к DSP, шлёт события
// рендера. Финальную перестройку VFO A делает вызывающий (UI: ApplyVfoA).
function MakeBandSettings: TBandSettings;
procedure SaveCurrentBand;
procedure RestoreBand(BandIdx: Integer);
// Полный HP-кадр в радио (RX/TX freq через трансвертор + drive + MOX). // Полный HP-кадр в радио (RX/TX freq через трансвертор + drive + MOX).
// Вызывается командами смены частоты/центра и UI после правки drive. // Вызывается командами смены частоты/центра и UI после правки drive.
procedure PushNetworkState; procedure PushNetworkState;
@@ -500,6 +510,151 @@ begin
Result := Min(Round(audio_vol * 1.02 * 255.0), 255); Result := Min(Round(audio_vol * 1.02 * 255.0), 255);
end; end;
procedure TRadioController.SanitizeFilterForMode;
// Для FM индексы фильтров только 0/1 (NFM/WFM); приводим FFilter и
// синхронизируем FFilterBW/FFMDeviation. Чистая (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;
end;
function TRadioController.MakeBandSettings: TBandSettings;
begin
Result.VfoA := FVfoA;
Result.VfoB := FVfoB;
Result.Mode := FMode;
Result.FilterIdx := FFilter;
Result.FilterBW := FFilterBW;
Result.AGCMode := FAGCMode;
Result.AGCTop := FAGCTop;
Result.CTun := FCTun;
Result.SpanHz := FSpanHz;
Result.FMSQOn := FFMSQOn;
Result.FMSQLevel := FFMSQLevel;
Result.CTCSSOn := FFMCTCSSOn;
Result.CTCSSToneIdx := FFMCTCSSToneIdx;
Result.FMStepOn := FFMStepOn;
Result.FMStepIdx := FFMStepIdx;
Result.FMRptDir := FFMRptDir;
Result.FMRptOffsetHz := FFMRptOffsetHz;
// Waterfall AGC/NF — глобальные (не диапазонные)
end;
procedure TRadioController.SaveCurrentBand;
// Сохраняет текущее состояние диапазона. В XVTR-режиме пишем в XVTR-слот
// (HF band-cache не трогаем — там сохранён HF-диапазон до активации XVTR).
var i: Integer;
begin
if not FDevConnected then Exit;
if FCurrentXvtr >= 0 then
begin
if (FCurrentXvtr < CFG_XVTR_COUNT) then
begin
i := FCurrentXvtr;
FXvtrSettings.Entries[i].LastFreq := FVfoA;
FXvtrSettings.Entries[i].LastMode := FMode;
FXvtrSettings.Entries[i].LastFilterIdx := FFilter;
FXvtrSettings.Entries[i].LastFMSQOn := FFMSQOn;
FXvtrSettings.Entries[i].LastFMSQLevel := FFMSQLevel;
FXvtrSettings.Entries[i].LastCTCSSOn := FFMCTCSSOn;
FXvtrSettings.Entries[i].LastCTCSSToneIdx := FFMCTCSSToneIdx;
FXvtrSettings.Entries[i].LastFMStepOn := FFMStepOn;
FXvtrSettings.Entries[i].LastFMStepIdx := FFMStepIdx;
FXvtrSettings.Entries[i].LastFMRptDir := FFMRptDir;
FXvtrSettings.Entries[i].LastFMRptOffsetHz := FFMRptOffsetHz;
FXvtrSettings.Entries[i].LastCTun := FCTun;
FXvtrSettings.Entries[i].LastAGCMode := FAGCMode;
FXvtrSettings.Entries[i].LastAGCTop := FAGCTop;
FXvtrSettings.Entries[i].LastCTCSSAutoActive := FFMCTCSSAutoActive;
FXvtrSettings.Entries[i].LastFMRptAutoActive := FFMRptAutoActive;
end;
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
Exit;
end;
// Не сохраняем, если VFO ушёл в VHF/UHF (например, после канала на 2м без
// XVTR) — иначе HF-кэш диапазона перезапишется VHF-частотой.
if FVfoA > 61000000 then Exit;
FBandCache[FCurrentBand] := MakeBandSettings;
FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]);
end;
procedure TRadioController.RestoreBand(BandIdx: Integer);
// Загружает состояние диапазона из кэша, применяет к DSP и шлёт события рендера.
// Финальную перестройку VFO A (с канальной оркестрацией) делает вызывающий
// UI-путь через ApplyVfoA(FVfoA); здесь FVfoA лишь выставляется.
var B: TBandSettings;
begin
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
B := FBandCache[BandIdx];
FCurrentBand := BandIdx;
// --- Режим ---
FMode := B.Mode;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
FFilter := B.FilterIdx;
FFilterBW := B.FilterBW;
SanitizeFilterForMode;
ApplyModeFilter;
// --- AGC (единый маппинг — исправляет каст-баг RestoreBand) ---
FAGCMode := B.AGCMode;
FAGCTop := B.AGCTop;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetAGCTop(FAGCTop);
ApplyAGCToEngine;
// --- FM squelch / CTCSS / step / repeater ---
FFMSQOn := B.FMSQOn;
FFMSQLevel := B.FMSQLevel;
FFMCTCSSOn := B.CTCSSOn;
FFMCTCSSToneIdx := B.CTCSSToneIdx;
if FWDSPReady and Assigned(FDSPEngine) then
begin
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
end;
FFMStepOn := B.FMStepOn;
FFMStepIdx := EnsureRange(B.FMStepIdx, 0, 3);
FFMRptDir := B.FMRptDir;
FFMRptOffsetHz := B.FMRptOffsetHz;
if FFMRptOffsetHz <= 0 then FFMRptOffsetHz := RptDefaultOffsetHz(B.VfoA);
// --- CTUN ---
FCTun := B.CTun;
if FWDSPReady and Assigned(FDSPEngine) and not FCTun then FDSPEngine.SetShift(0.0);
// --- Span (sample rate глобальный, не per-band — лишь ресинк) ---
FSpanHz := FSampleRate;
// --- VFO / drive ---
FCenterFreq := B.VfoA;
FVfoB := B.VfoB;
FVfoA := B.VfoA;
FDriveLevel := CalcDriveByte;
// --- Рендер: единичные события (UI рисует через OnControllerState) ---
Changed(rfBand);
Changed(rfMode); // rfMode-рендер зовёт UpdateFilterButtons (FM-панели)
Changed(rfFilter);
Changed(rfAGCMode);
Changed(rfAGCTop);
Changed(rfFMSQ);
Changed(rfFMSQLevel);
Changed(rfFMCTCSS);
Changed(rfFMCTCSSTone);
Changed(rfFMStep); // кнопка вкл/выкл STEP
Changed(rfFMStepIdx); // дропдаун шага + web + grid
Changed(rfFMRpt);
Changed(rfCTun);
Changed(rfVfoB);
// rfVfoA НЕ шлём: финальную перестройку делает ApplyVfoA в UI-обёртке.
end;
function TRadioController.ActiveVfoHz: Double; function TRadioController.ActiveVfoHz: Double;
begin begin
if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB; if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB;