Phase 5: make XVTR enter/exit full controller commands

Move the xvtr wrapper render-extras (wideband view + web push) into the rfXvtr
render handler, and the HF-return (RestoreBand) into core DeactivateXvtr — emitting
rfXvtr *after* RestoreBand so the extras render sees the settled HF state.
MainForm.ActivateXvtrBand/DeactivateXvtr collapse to thin delegates.

ChannelController drops its OnActivateXvtr/OnDeactivateXvtr host callbacks and calls
FRadio.SetXvtrBand directly — no host coupling left, only the OnActiveChanged out-event.

Also fixes a latent regression from the OnAfterTune hook: an intermediate SetVfoA
during channel Apply (xvtr-exit → RestoreBand → retune to the old freq) fired
OnVfoTuned and deactivated the channel mid-apply. Apply is now atomic w.r.t.
channel orchestration via an FApplying guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Uladzimir Karpenka
2026-06-09 09:59:20 +03:00
co-authored by Claude Opus 4.8
parent de1f4b772d
commit cc8380ae85
3 changed files with 39 additions and 38 deletions
+17 -14
View File
@@ -12,10 +12,8 @@ unit ChannelController;
// + мощность «до канала») — это снимает последний UI-блокер для headless-режима. // + мощность «до канала») — это снимает последний UI-блокер для headless-режима.
// //
// Наружу юнит эмитит собственное событие OnActiveChanged (рендер кнопки Channel). // Наружу юнит эмитит собственное событие OnActiveChanged (рендер кнопки Channel).
// Две UI-композитные XVTR-операции (вход/выход трансвертера с wideband-видом и // Вход/выход трансвертера идёт через FRadio.SetXvtrBand (команда несёт DSP/сеть +
// web-пушем) пока живут в MainForm-обёртках — юнит зовёт их через колбэки // рендер/wideband/web через rfXvtr) — host-колбэков у юнита больше нет.
// OnActivateXvtr/OnDeactivateXvtr (сократятся при переносе xvtr render-extras в
// OnControllerState).
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
{$mode delphi}{$H+} {$mode delphi}{$H+}
@@ -29,8 +27,6 @@ uses
type type
// Рендер активного канала: Active=True → подсветка + Name; False → сброс. // Рендер активного канала: Active=True → подсветка + Name; False → сброс.
TChannelActiveEvent = procedure(Idx: Integer; const Name: string; Active: Boolean) of object; TChannelActiveEvent = procedure(Idx: Integer; const Name: string; Active: Boolean) of object;
TChannelXvtrEvent = procedure(Idx: Integer) of object;
TChannelNotifyEvent = procedure of object;
TChannelController = class TChannelController = class
private private
@@ -38,9 +34,8 @@ type
FActiveIdx: Integer; // -1 = нет активного канала FActiveIdx: Integer; // -1 = нет активного канала
FPreChannelPower: Integer; // drive % до применения канала (-1 = не сохранён) FPreChannelPower: Integer; // drive % до применения канала (-1 = не сохранён)
FRestoringDrive: Boolean; // True пока юнит сам ставит drive (apply/restore) FRestoringDrive: Boolean; // True пока юнит сам ставит drive (apply/restore)
FApplying: Boolean; // True пока идёт Apply (гасит промежуточный OnVfoTuned)
FOnActiveChanged: TChannelActiveEvent; FOnActiveChanged: TChannelActiveEvent;
FOnActivateXvtr: TChannelXvtrEvent;
FOnDeactivateXvtr: TChannelNotifyEvent;
procedure EmitActive(Active: Boolean; const Name: string); procedure EmitActive(Active: Boolean; const Name: string);
procedure SetDriveGuarded(V: Integer); // SetDrive, не сбрасывающий память канала procedure SetDriveGuarded(V: Integer); // SetDrive, не сбрасывающий память канала
public public
@@ -54,8 +49,6 @@ type
property ActiveIdx: Integer read FActiveIdx write FActiveIdx; property ActiveIdx: Integer read FActiveIdx write FActiveIdx;
property OnActiveChanged: TChannelActiveEvent read FOnActiveChanged write FOnActiveChanged; property OnActiveChanged: TChannelActiveEvent read FOnActiveChanged write FOnActiveChanged;
property OnActivateXvtr: TChannelXvtrEvent read FOnActivateXvtr write FOnActivateXvtr;
property OnDeactivateXvtr: TChannelNotifyEvent read FOnDeactivateXvtr write FOnDeactivateXvtr;
end; end;
implementation implementation
@@ -107,6 +100,12 @@ procedure TChannelController.Apply(const Ch: TChannel);
var var
i, NewBand, XvtrIdx: Integer; i, NewBand, XvtrIdx: Integer;
begin begin
// Промежуточные retune внутри Apply (XVTR-выход → RestoreBand → SetVfoA на
// СТАРУЮ частоту) фаерят OnAfterTune→OnVfoTuned; без этого гарда CheckActive
// деактивировал бы канал прямо посреди его применения. Финальное состояние
// (FActiveIdx + Ch.RXFreq) выставляем сами — оркестрация снаружи Apply.
FApplying := True;
try
// Сохраняем текущее состояние до любых изменений // Сохраняем текущее состояние до любых изменений
FRadio.SaveCurrentBand; FRadio.SaveCurrentBand;
@@ -125,15 +124,15 @@ begin
if XvtrIdx >= 0 then if XvtrIdx >= 0 then
begin begin
// Канал на трансвертерном диапазоне — активируем XVTR если не активен // Канал на трансвертерном диапазоне — активируем XVTR если не активен
if (FRadio.FCurrentXvtr <> XvtrIdx) and Assigned(FOnActivateXvtr) then if FRadio.FCurrentXvtr <> XvtrIdx then
FOnActivateXvtr(XvtrIdx); FRadio.SetXvtrBand(XvtrIdx); // вход в слот + рендер/wideband/web (rfXvtr)
end end
else else
begin begin
// Канал на HF (или неизвестная частота). Если были на XVTR — выходим в HF, // Канал на HF (или неизвестная частота). Если были на XVTR — выходим в HF,
// иначе SetVfoA заклэмпит частоту. // иначе SetVfoA заклэмпит частоту.
if (FRadio.FCurrentXvtr >= 0) and Assigned(FOnDeactivateXvtr) then if FRadio.FCurrentXvtr >= 0 then
FOnDeactivateXvtr; FRadio.SetXvtrBand(-1); // выход в HF (DeactivateXvtr несёт HF-возврат)
NewBand := FreqToBandIdx(Ch.RXFreq); NewBand := FreqToBandIdx(Ch.RXFreq);
if (NewBand >= 0) and (NewBand <> FRadio.FCurrentBand) then if (NewBand >= 0) and (NewBand <> FRadio.FCurrentBand) then
FRadio.SetCurrentBandIdx(NewBand); // band-индекс + drive-кал + рендер кнопок FRadio.SetCurrentBandIdx(NewBand); // band-индекс + drive-кал + рендер кнопок
@@ -170,6 +169,9 @@ begin
// Сохраняем итог в band/XVTR-кэш // Сохраняем итог в band/XVTR-кэш
if (XvtrIdx >= 0) or (NewBand >= 0) then if (XvtrIdx >= 0) or (NewBand >= 0) then
FRadio.SaveCurrentBand; FRadio.SaveCurrentBand;
finally
FApplying := False;
end;
end; end;
procedure TChannelController.CheckActive; procedure TChannelController.CheckActive;
@@ -217,6 +219,7 @@ procedure TChannelController.OnVfoTuned;
// Без активного канала: сброс авто-CTCSS + восстановление pre-channel drive. // Без активного канала: сброс авто-CTCSS + восстановление pre-channel drive.
// С активным каналом: CheckActive деактивирует при уходе с частоты канала. // С активным каналом: CheckActive деактивирует при уходе с частоты канала.
begin begin
if FApplying then Exit; // промежуточные retune внутри Apply — не трогаем канал
if FActiveIdx < 0 then if FActiveIdx < 0 then
begin begin
if FRadio.FFMCTCSSAutoActive then if FRadio.FFMCTCSSAutoActive then
+15 -21
View File
@@ -756,8 +756,6 @@ begin
// XVTR-вход/выход — через UI-обёртки (wideband/web живут в них). // XVTR-вход/выход — через UI-обёртки (wideband/web живут в них).
FChannelController := TChannelController.Create(FController); FChannelController := TChannelController.Create(FController);
FChannelController.OnActiveChanged := OnChannelActiveChanged; FChannelController.OnActiveChanged := OnChannelActiveChanged;
FChannelController.OnActivateXvtr := ActivateXvtrBand;
FChannelController.OnDeactivateXvtr := DeactivateXvtr;
// Канал-оркестрация фаером из контроллера в конце SetVfoA (любой путь тюнинга). // Канал-оркестрация фаером из контроллера в конце SetVfoA (любой путь тюнинга).
FController.OnAfterTune := FChannelController.OnVfoTuned; FController.OnAfterTune := FChannelController.OnVfoTuned;
FController.FVfoA := 14200000; FController.FVfoA := 14200000;
@@ -1218,9 +1216,17 @@ begin
StyleButton(BtnBand[i], StyleButton(BtnBand[i],
(FController.FCurrentXvtr < 0) and (i = FController.FCurrentBand)); (FController.FCurrentXvtr < 0) and (i = FController.FCurrentBand));
rfXvtr: rfXvtr:
for i := 0 to CFG_XVTR_COUNT - 1 do begin
if BtnXvtrBand[i] <> nil then for i := 0 to CFG_XVTR_COUNT - 1 do
StyleButton(BtnXvtrBand[i], i = FController.FCurrentXvtr); if BtnXvtrBand[i] <> nil then
StyleButton(BtnXvtrBand[i], i = FController.FCurrentXvtr);
// Wideband-вид + web-зеркало xvtr (ранее в обёртках Activate/Deactivate).
// rfXvtr эмитят ТОЛЬКО ActivateXvtr/DeactivateXvtr — состояние финальное.
UpdateWidebandFrequencyView;
if PbWideband <> nil then PbWideband.Invalidate;
if PbWidebandRuler <> nil then PbWidebandRuler.Invalidate;
PushXvtrToWeb;
end;
rfBandRestore: rfBandRestore:
// Явная смена диапазона: сброс порогов водопада (не на каждом тюне). // Явная смена диапазона: сброс порогов водопада (не на каждом тюне).
FSpecView.ResetWfAvgBuf; FSpecView.ResetWfAvgBuf;
@@ -5141,13 +5147,9 @@ end;
// середине [FreqBegin..FreqEnd], применяет network XVTR-режим. // середине [FreqBegin..FreqEnd], применяет network XVTR-режим.
procedure TMainForm.ActivateXvtrBand(Idx: Integer); procedure TMainForm.ActivateXvtrBand(Idx: Integer);
begin begin
// Вход в XVTR-слот (state+DSP+VFO+сеть) — в контроллере; рендер кнопок/частот/ // Тонкий делегат: вход в XVTR-слот + рендер кнопок/частот/спектра/wideband/web —
// спектра — через события (rfXvtr/rfBand/rfActiveVfo). Здесь только UI: wideband+web. // всё в команде/событиях (rfXvtr/rfBand/rfActiveVfo).
FController.ActivateXvtr(Idx); FController.ActivateXvtr(Idx);
UpdateWidebandFrequencyView;
if PbWideband <> nil then PbWideband.Invalidate;
if PbWidebandRuler <> nil then PbWidebandRuler.Invalidate;
PushXvtrToWeb;
end; end;
// Деактивирует XVTR — возвращает к HF-режиму. FController.FCurrentBand указывает // Деактивирует XVTR — возвращает к HF-режиму. FController.FCurrentBand указывает
@@ -5155,17 +5157,9 @@ end;
// VFO/Mode/Filter/AGC/CTUN/Span этого диапазона и обновит подсветку HF band-кнопки. // VFO/Mode/Filter/AGC/CTUN/Span этого диапазона и обновит подсветку HF band-кнопки.
procedure TMainForm.DeactivateXvtr; procedure TMainForm.DeactivateXvtr;
begin begin
if FController.FCurrentXvtr < 0 then Exit; // Тонкий делегат: выход в HF (SaveCurrentBand + HF-возврат RestoreBand + рендер
// Сохранение слота + выход (+ гашение XVTR-кнопок через rfXvtr) — в контроллере. // кнопок/wideband/web) — всё в команде/событиях.
FController.DeactivateXvtr; FController.DeactivateXvtr;
// HF-возврат: состояние/DSP из кэша + финальная перестройка активного VFO
// (через UI-путь RestoreBand: ResetWfAvgBuf + controller.RestoreBand + ApplyVfoA/SetVfoB).
if (FController.FCurrentBand >= 0) and (FController.FCurrentBand < CFG_BAND_COUNT) then
RestoreBand(FController.FCurrentBand);
UpdateWidebandFrequencyView;
if PbWideband <> nil then PbWideband.Invalidate;
if PbWidebandRuler <> nil then PbWidebandRuler.Invalidate;
PushXvtrToWeb;
end; end;
procedure TMainForm.OnXvtrSettingsChange(const X: TXvtrSettings); procedure TMainForm.OnXvtrSettingsChange(const X: TXvtrSettings);
+7 -3
View File
@@ -1631,15 +1631,19 @@ begin
end; end;
procedure TRadioController.DeactivateXvtr; procedure TRadioController.DeactivateXvtr;
// Сохраняет текущий XVTR-слот и выходит в HF. Сам HF-возврат (RestoreBand + // Сохраняет текущий XVTR-слот и выходит в HF: HF-возврат (RestoreBand + финальная
// финальная перестройка активного VFO) делает UI-обёртка — там же ResetWfAvgBuf. // перестройка активного VFO + ResetWfAvgBuf через rfBandRestore) делается здесь.
// Changed(rfXvtr) шлём ПОСЛЕ RestoreBand — чтобы wideband/web-extras в его рендере
// увидели уже восстановленное HF-состояние.
begin begin
if FCurrentXvtr < 0 then Exit; if FCurrentXvtr < 0 then Exit;
SaveCurrentBand; // XVTR-ветка: пишет весь слот + SaveXvtr SaveCurrentBand; // XVTR-ветка: пишет весь слот + SaveXvtr
if FDevConnected then FSettings.Save; if FDevConnected then FSettings.Save;
FCurrentXvtr := -1; FCurrentXvtr := -1;
ApplyXvtrToNetwork; // снять XVTR enable bit ApplyXvtrToNetwork; // снять XVTR enable bit
Changed(rfXvtr); // погасить XVTR-кнопки if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
RestoreBand(FCurrentBand); // HF-возврат (состояние/DSP/retune)
Changed(rfXvtr); // погасить XVTR-кнопки + wideband/web (rfXvtr-рендер)
end; end;
procedure TRadioController.SetXvtrBand(Idx: Integer); procedure TRadioController.SetXvtrBand(Idx: Integer);