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 SaveWindowBounds;
function MakeGlobalSettings: TGlobalSettings;
function MakeBandSettings: TBandSettings;
procedure FreqDispBChanged(Sender: TObject; NewFreq: Int64);
procedure BtnBandClick(Sender: TObject);
procedure BtnModeClick(Sender: TObject);
@@ -787,28 +786,6 @@ begin
FController.FSettings.SaveWindowBounds(L, T, Wd, Ht, DPI, Maximized);
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;
var i: Integer;
begin
@@ -889,124 +866,21 @@ end;
procedure TMainForm.SaveCurrentBand;
begin
if not FController.FDevConnected then Exit;
// В XVTR-режиме НЕ трогаем HF band-cache: FController.FBandCache[FController.FCurrentBand]
// относится к 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]);
// Логика сохранения (band-cache/XVTR-слот + конфиг) — в контроллере.
FController.SaveCurrentBand;
end;
procedure TMainForm.RestoreBand(BandIdx: Integer);
var
B: TBandSettings;
i: Integer;
begin
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
B := FController.FBandCache[BandIdx];
// --- Кнопки диапазонов ---
for i := 0 to BAND_COUNT - 1 do
StyleButton(BtnBand[i], i = BandIdx);
// Сбрасываем waterfall thresholds при смене диапазона
// Сброс порогов водопада — только при явной смене диапазона (не на каждом
// пересечении границы при тюнинге, поэтому здесь, а не в rfBand-рендере).
FSpecView.ResetWfAvgBuf;
// --- Режим: сначала устанавливаем FController.FMode, потом кнопки и WDSP ---
FController.FMode := B.Mode;
for i := 0 to MODE_COUNT - 1 do
StyleButton(BtnMode[i], i = FController.FMode);
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));
// Состояние + DSP + события рендера — в контроллере.
FController.RestoreBand(BandIdx);
// Финальная перестройка VFO A с канальной оркестрацией (auto-CTCSS,
// pre-channel drive) — через UI-путь ApplyVfoA.
ApplyVfoA(Round(FController.FVfoA));
end;
procedure TMainForm.SaveAllAndExit;
@@ -1492,6 +1366,12 @@ begin
if (FController.FMode = MODE_FM) and (FSpecView <> nil) then
FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx];
end;
rfFMStep:
if BtnFMStep <> nil then
begin
StyleButton(BtnFMStep, FController.FFMStepOn);
BtnFMStep.Repaint;
end;
rfFMSQ:
begin
StyleButton(BtnFMSQ, FController.FFMSQOn);