From e09bc21bbaa04ebf1256295dad05abbe881e14d1 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Fri, 5 Jun 2026 12:45:11 +0300 Subject: [PATCH] Fix active-VFO desync; add bandstack-on-VFO-switch and per-XVTR VFO-B Active-VFO desync (batch 15 follow-up): DoConnectDevice loaded FActiveVfo from settings but never re-rendered the highlight (it was drawn once in FormCreate while the field was still 0) nor re-tuned the receiver. Result on reconnect with B last-active: the engine (spectrum, filter band, mouse tuning) ran on VFO B while the UI showed A active. Now the connect path brings both the receiver and the highlight to the actually-active VFO via SetActiveVfo, and RestoreBand's final retune follows the active VFO (A -> ApplyVfoA, B -> SetVfoB) instead of always A. Bandstack on VFO switch: switching the active VFO to one parked on a different ham band now restores that band's DSP state (mode/filter/AGC/ CTUN/FM). Extracted ApplyBandDSP (the DSP half of RestoreBand, without the VFO frequencies) and reused it from both RestoreBand and SetActiveVfo. VFO frequencies stay independent (each VFO carries its own band); continuous tuning across a band edge still does not recall (band button only) as on real radios. SaveCurrentBand no longer writes a VFO frequency into a band's memory unless that VFO is actually within the band, so a 40m VFO-A no longer pollutes 30m's stored VfoA. Per-transverter VFO-B: TXvtrEntry gains LastFreqB (load/save/default + range clamp). ActivateXvtrBand restores VFO B, Deactivate/SaveAllAndExit persist it, and SetVfoB clamps to the transverter range like SetVfoA. The connect-time guard that forced VFO A in XVTR is dropped since B is now a valid, in-range frequency there too. Co-Authored-By: Claude Opus 4.8 --- MainForm.pas | 31 +++++++++++-- RadioController.pas | 110 +++++++++++++++++++++++++++++++------------- Settings.pas | 13 +++++- 3 files changed, 114 insertions(+), 40 deletions(-) diff --git a/MainForm.pas b/MainForm.pas index 85b595e..2466712 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -878,9 +878,14 @@ begin FSpecView.ResetWfAvgBuf; // Состояние + DSP + события рендера — в контроллере. FController.RestoreBand(BandIdx); - // Финальная перестройка VFO A с канальной оркестрацией (auto-CTCSS, - // pre-channel drive) — через UI-путь ApplyVfoA. - ApplyVfoA(Round(FController.FVfoA)); + // Финальная перестройка приёмника на АКТИВНЫЙ VFO. Если активен B — нельзя + // центрироваться на VFO A (иначе движок слушает A, а маркер/мышь — на B). + // VFO A идёт через ApplyVfoA (канальная оркестрация: auto-CTCSS, pre-channel + // drive); VFO B — через SetVfoB (перестройка + рендер rfVfoB). + if FController.FActiveVfo = 0 then + ApplyVfoA(Round(FController.FVfoA)) + else + FController.SetVfoB(Round(FController.FVfoB)); end; procedure TMainForm.SaveAllAndExit; @@ -891,9 +896,12 @@ begin FController.FSettings.SaveGlobal(FController.FDevMAC, MakeGlobalSettings); if FController.FDevConnected then FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings); - // Запомнить LastFreq XVTR для следующего запуска + // Запомнить LastFreq A/B XVTR для следующего запуска if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) then - FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFreq := FController.FVfoA; + begin + FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFreq := FController.FVfoA; + FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFreqB := FController.FVfoB; + end; FController.FSettings.SaveXvtr(FController.FDevMAC, FController.FXvtrSettings); FController.FSettings.Save; end; @@ -4140,6 +4148,15 @@ begin and FController.FXvtrSettings.Entries[G_Settings.LastXvtr].Enabled then ActivateXvtrBand(G_Settings.LastXvtr); + // Активный VFO загружен из настроек (выше), но подсветку рисовали в FormCreate + // (тогда FActiveVfo ещё = 0 → A), а RestoreBand/ActivateXvtrBand перестраивают + // только VFO A. Приводим приёмник И подсветку к фактически активному VFO, + // иначе движок работает по B, а UI показывает активным A. + if FController.FActiveVfo = 1 then + FController.SetActiveVfo(1) // перестройка RX на VFO B + рендер (rfActiveVfo) + else + UpdateVfoDisplay; + SetStatusText(1, 'IP: ' + FController.FNetwork.Device.IPAddress); SetStatusText(0, 'Board: ' + BoardTypeName(FController.FNetwork.Device.BoardType)); @@ -6562,6 +6579,9 @@ begin // CTUN FController.FCTun := E.LastCTun; StyleButton(BtnCTun, FController.FCTun); + // VFO-B: своя сохранённая частота слота, клэмп в диапазон трансвертера. + FController.FVfoB := EnsureRange(E.LastFreqB, E.FreqBegin, E.FreqEnd); + FreqDispB.Frequency := Round(FController.FVfoB); // VFO на видимую частоту; FController.FCenterFreq тоже visible FController.FVfoA := Vis; FController.FCenterFreq := Vis; @@ -6600,6 +6620,7 @@ begin if FController.FCurrentXvtr < CFG_XVTR_COUNT then begin FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFreq := FController.FVfoA; + FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFreqB := FController.FVfoB; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastMode := FController.FMode; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFilterIdx := FController.FFilter; FController.FXvtrSettings.Entries[FController.FCurrentXvtr].LastFMSQOn := FController.FFMSQOn; diff --git a/RadioController.pas b/RadioController.pas index 0f6493b..042bcbe 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -231,6 +231,10 @@ type function MakeBandSettings: TBandSettings; procedure SaveCurrentBand; procedure RestoreBand(BandIdx: Integer); + // Применяет DSP-настройки диапазона (mode/filter/AGC/CTUN/FM) + шлёт события + // рендера. НЕ трогает частоты VFO/центр — это переиспользуется как RestoreBand + // (с загрузкой VFO), так и SetActiveVfo (bandstack: смена бэнда без сброса VFO). + procedure ApplyBandDSP(const B: TBandSettings); // Полный HP-кадр в радио (RX/TX freq через трансвертор + drive + MOX). // Вызывается командами смены частоты/центра и UI после правки drive. @@ -547,7 +551,9 @@ end; procedure TRadioController.SaveCurrentBand; // Сохраняет текущее состояние диапазона. В XVTR-режиме пишем в XVTR-слот // (HF band-cache не трогаем — там сохранён HF-диапазон до активации XVTR). -var i: Integer; +var + i: Integer; + B: TBandSettings; begin if not FDevConnected then Exit; if FCurrentXvtr >= 0 then @@ -556,6 +562,7 @@ begin begin i := FCurrentXvtr; FXvtrSettings.Entries[i].LastFreq := FVfoA; + FXvtrSettings.Entries[i].LastFreqB := FVfoB; FXvtrSettings.Entries[i].LastMode := FMode; FXvtrSettings.Entries[i].LastFilterIdx := FFilter; FXvtrSettings.Entries[i].LastFMSQOn := FFMSQOn; @@ -578,20 +585,19 @@ begin // Не сохраняем, если VFO ушёл в VHF/UHF (например, после канала на 2м без // XVTR) — иначе HF-кэш диапазона перезапишется VHF-частотой. if FVfoA > 61000000 then Exit; - FBandCache[FCurrentBand] := MakeBandSettings; + B := MakeBandSettings; + // Bandstack: VFO A и B независимы и могут стоять на разных бэндах. В кэш + // ЭТОГО бэнда пишем только ту частоту VFO, что реально в нём — иначе чужая + // частота (напр. VFO A на 40м при сохранении 30м) загрязнит memory бэнда. + if FreqToBandIdx(FVfoA) <> FCurrentBand then B.VfoA := FBandCache[FCurrentBand].VfoA; + if FreqToBandIdx(FVfoB) <> FCurrentBand then B.VfoB := FBandCache[FCurrentBand].VfoB; + FBandCache[FCurrentBand] := B; FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]); end; -procedure TRadioController.RestoreBand(BandIdx: Integer); -// Загружает состояние диапазона из кэша, применяет к DSP и шлёт события рендера. -// Финальную перестройку VFO A (с канальной оркестрацией) делает вызывающий -// UI-путь через ApplyVfoA(FVfoA); здесь FVfoA лишь выставляется. -var B: TBandSettings; +procedure TRadioController.ApplyBandDSP(const B: TBandSettings); +// DSP-настройки диапазона + события рендера. Частоты VFO/центр НЕ трогает. 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); @@ -631,12 +637,6 @@ begin // --- 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-панели) @@ -651,6 +651,26 @@ begin Changed(rfFMStepIdx); // дропдаун шага + web + grid Changed(rfFMRpt); Changed(rfCTun); +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; + + ApplyBandDSP(B); + + // --- VFO / drive (только RestoreBand — bandstack-путь VFO не трогает) --- + FCenterFreq := B.VfoA; + FVfoB := B.VfoB; + FVfoA := B.VfoA; + FDriveLevel := CalcDriveByte; + Changed(rfVfoB); // rfVfoA НЕ шлём: финальную перестройку делает ApplyVfoA в UI-обёртке. end; @@ -743,9 +763,21 @@ procedure TRadioController.SetVfoB(Hz: Double); // ApplyTuneCore (с учётом CTUN), детектим диапазон и толкаем в сеть. Если B // неактивен (split, слушаем A) — только сохраняем частоту (для TX). // Рендер FreqDispB/спектра — в UI через OnControllerState(rfVfoB). -var BandIdx: Integer; +var + BandIdx: Integer; + NewFreq: Int64; begin - FVfoB := Hz; + NewFreq := Round(Hz); + // Клэмп в пределах XVTR-band (как в SetVfoA) — VFO B тоже не уходит за край. + if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and + FXvtrSettings.Entries[FCurrentXvtr].Enabled then + begin + if NewFreq < FXvtrSettings.Entries[FCurrentXvtr].FreqBegin then + NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqBegin); + if NewFreq > FXvtrSettings.Entries[FCurrentXvtr].FreqEnd then + NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqEnd); + end; + FVfoB := NewFreq; if FActiveVfo = 1 then begin ApplyTuneCore(FVfoB); @@ -766,17 +798,36 @@ end; procedure TRadioController.SetActiveVfo(Idx: Integer); // Переключение активного (RX) VFO с перестройкой приёмника на его частоту. -// CTUN: если частота нового VFO видна в текущем окне — только меняем shift +// Bandstack: если активируемый VFO стоит на ДРУГОМ ham-бэнде — сохраняем +// покидаемый бэнд и восстанавливаем DSP-настройки нового (mode/filter/AGC/CTUN/ +// FM). Частоты обоих VFO при этом НЕ трогаем — каждый VFO несёт свой диапазон. +// CTUN (того же бэнда): если частота нового VFO видна в окне — только shift // (DDC/сеть не трогаем); иначе CTUN сбрасывается и центрируемся на частоте. // Рендер (дисплеи/спектр/band/CTUN-кнопка) — в UI через OnControllerState. var ActiveFreq: Double; HalfSpan, Offset: Double; - FreqVisible: Boolean; - BandIdx: Integer; + FreqVisible, NeedPush: Boolean; + NewBand: Integer; begin FActiveVfo := Idx; ActiveFreq := ActiveVfoHz; + NeedPush := False; + + // --- Bandstack: активируемый VFO на другом ham-бэнде → восстановить бэнд --- + if FCurrentXvtr < 0 then + begin + NewBand := FreqToBandIdx(ActiveFreq); + if (NewBand >= 0) and (NewBand <> FCurrentBand) then + begin + SaveCurrentBand; // сохранить покидаемый бэнд + FCurrentBand := NewBand; + ApplyBandDSP(FBandCache[NewBand]); // mode/filter/AGC/CTUN/FM + рендер + FCenterFreq := ActiveFreq; // центр на активную (CTUN центрируется тут) + FDriveLevel := CalcDriveByte; // PA-калибровка нового бэнда + NeedPush := True; // DDC-центр сменился → толкнуть в сеть + end; + end; if FCTun then begin @@ -785,7 +836,7 @@ begin FreqVisible := (Offset > -HalfSpan) and (Offset < HalfSpan); if FreqVisible then begin - // Частота видна — просто сдвигаем демодулятор, DDC/сеть не трогаем. + // Частота видна — просто сдвигаем демодулятор. if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset); end else @@ -795,7 +846,7 @@ begin FBandCache[FCurrentBand].CTun := False; FCenterFreq := ActiveFreq; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0); - PushNetworkState; + NeedPush := True; Changed(rfCTun); end; end @@ -803,17 +854,10 @@ begin begin FCenterFreq := ActiveFreq; if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0); - PushNetworkState; - end; - - // Детект диапазона по новой частоте (без пересчёта drive — как исторически). - BandIdx := FreqToBandIdx(ActiveFreq); - if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then - begin - FCurrentBand := BandIdx; - Changed(rfBand); + NeedPush := True; end; + if NeedPush then PushNetworkState; Changed(rfActiveVfo); end; diff --git a/Settings.pas b/Settings.pas index c724984..d4e717d 100644 --- a/Settings.pas +++ b/Settings.pas @@ -161,7 +161,8 @@ type TXPower: Integer; // 0..100 % (drive level специально для XVTR) DisablePA: Boolean; // True — отключить внутренний PA при TX (XVTR имеет свой) RXAntenna: Byte; // 0=по-умолчанию(основной band), 1=ANT1, 2=ANT2, 3=ANT3 - LastFreq: Double; // Hz — последняя tuned visible частота (BandStack) + LastFreq: Double; // Hz — последняя tuned visible частота VFO A (BandStack) + LastFreqB: Double; // Hz — последняя tuned visible частота VFO B LastMode: Integer; // Mode (LSB=0,USB=1,…FM=5) LastFilterIdx: Integer; // FM: 0=NFM, 1=WFM; иначе 0..FILT_COUNT-1 LastFMSQOn: Boolean; @@ -1205,6 +1206,7 @@ begin X.Entries[i].DisablePA := False; X.Entries[i].RXAntenna := 0; X.Entries[i].LastFreq := 0.0; + X.Entries[i].LastFreqB := 0.0; X.Entries[i].LastMode := 1; // USB — VHF SSB calling default X.Entries[i].LastFilterIdx := 0; // NFM X.Entries[i].LastFMSQOn := False; @@ -1227,11 +1229,13 @@ begin X.Entries[0].FreqEnd := 148000000; X.Entries[0].LOOffset := 116000000; X.Entries[0].LastFreq := 144300000; + X.Entries[0].LastFreqB := 145500000; X.Entries[1].ButtonText := '70cm'; X.Entries[1].FreqBegin := 430000000; X.Entries[1].FreqEnd := 440000000; X.Entries[1].LOOffset := 404000000; X.Entries[1].LastFreq := 432200000; + X.Entries[1].LastFreqB := 433000000; end; function TSettingsManager.LoadXvtr(const MAC: array of Byte; @@ -1265,6 +1269,7 @@ begin X.Entries[i].DisablePA := JB(EObj, 'dis_pa', X.Entries[i].DisablePA); X.Entries[i].RXAntenna := EnsureRange(JI(EObj, 'rx_ant', X.Entries[i].RXAntenna), 0, 3); X.Entries[i].LastFreq := JD(EObj, 'last_freq', X.Entries[i].LastFreq); + X.Entries[i].LastFreqB := JD(EObj, 'last_freq_b', X.Entries[i].LastFreqB); X.Entries[i].LastMode := EnsureRange(JI(EObj, 'last_mode', X.Entries[i].LastMode), 0, 7); X.Entries[i].LastFilterIdx := EnsureRange(JI(EObj, 'last_filt', X.Entries[i].LastFilterIdx), 0, 9); X.Entries[i].LastFMSQOn := JB(EObj, 'last_fmsq_on', X.Entries[i].LastFMSQOn); @@ -1280,10 +1285,13 @@ begin X.Entries[i].LastFMRptOffsetHz := JD(EObj, 'last_fmrpt_offset', X.Entries[i].LastFMRptOffsetHz); X.Entries[i].LastCTCSSAutoActive := JB(EObj, 'last_ctcss_auto', X.Entries[i].LastCTCSSAutoActive); X.Entries[i].LastFMRptAutoActive := JB(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive); - // Валидация: LastFreq должен быть в [FreqBegin..FreqEnd] + // Валидация: Last freqs должны быть в [FreqBegin..FreqEnd] if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or (X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then X.Entries[i].LastFreq := (X.Entries[i].FreqBegin + X.Entries[i].FreqEnd) / 2.0; + if (X.Entries[i].LastFreqB < X.Entries[i].FreqBegin) or + (X.Entries[i].LastFreqB > X.Entries[i].FreqEnd) then + X.Entries[i].LastFreqB := (X.Entries[i].FreqBegin + X.Entries[i].FreqEnd) / 2.0; end; end; @@ -1310,6 +1318,7 @@ begin JW(EObj, 'dis_pa', X.Entries[i].DisablePA); JW(EObj, 'rx_ant', Integer(X.Entries[i].RXAntenna)); JW(EObj, 'last_freq', X.Entries[i].LastFreq); + JW(EObj, 'last_freq_b', X.Entries[i].LastFreqB); JW(EObj, 'last_mode', X.Entries[i].LastMode); JW(EObj, 'last_filt', X.Entries[i].LastFilterIdx); JW(EObj, 'last_fmsq_on', X.Entries[i].LastFMSQOn);