Files
ewsdr/ChannelController.pas
Uladzimir KarpenkaandClaude Opus 4.8 cc8380ae85 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>
2026-06-09 09:59:20 +03:00

240 lines
10 KiB
ObjectPascal

unit ChannelController;
// -----------------------------------------------------------------------------
// TChannelController — адаптер каналов памяти поверх TRadioController.
//
// Каналы концептуально — recall сохранённого конфига (частота/режим/FM/мощность),
// т.е. клиент радио-ядра, а не часть DSP/сети. Поэтому логика применения каналов
// живёт здесь, отдельным юнитом, и оркеструет ПУБЛИЧНЫЕ команды контроллера
// (SetMode/SetFMCTCSS/SetFMRpt/SetVfoA/SetDrive/SaveCurrentBand/…). Каждая
// команда несёт DSP+сеть+состояние и эмитит Changed(field) — рендер делает UI
// через OnStateChanged. Юнит держит ТОЛЬКО канальное состояние (активный индекс
// + мощность «до канала») — это снимает последний UI-блокер для headless-режима.
//
// Наружу юнит эмитит собственное событие OnActiveChanged (рендер кнопки Channel).
// Вход/выход трансвертера идёт через FRadio.SetXvtrBand (команда несёт DSP/сеть +
// рендер/wideband/web через rfXvtr) — host-колбэков у юнита больше нет.
// -----------------------------------------------------------------------------
{$mode delphi}{$H+}
interface
uses
Classes, SysUtils, Math,
RadioController, ChannelStore, Settings, WDSPEngine, FMRepeater, BoardUtils;
type
// Рендер активного канала: Active=True → подсветка + Name; False → сброс.
TChannelActiveEvent = procedure(Idx: Integer; const Name: string; Active: Boolean) of object;
TChannelController = class
private
FRadio: TRadioController;
FActiveIdx: Integer; // -1 = нет активного канала
FPreChannelPower: Integer; // drive % до применения канала (-1 = не сохранён)
FRestoringDrive: Boolean; // True пока юнит сам ставит drive (apply/restore)
FApplying: Boolean; // True пока идёт Apply (гасит промежуточный OnVfoTuned)
FOnActiveChanged: TChannelActiveEvent;
procedure EmitActive(Active: Boolean; const Name: string);
procedure SetDriveGuarded(V: Integer); // SetDrive, не сбрасывающий память канала
public
constructor Create(ARadio: TRadioController);
procedure Select(Idx: Integer); // выбор канала из дропдауна
procedure Apply(const Ch: TChannel); // применить канал к радио
procedure CheckActive; // деактивировать при уходе с частоты
procedure OnVfoTuned; // канальная оркестрация после SetVfoA
procedure OnDriveChanged; // реакция на rfDrive: внешнее изменение снимает память
property ActiveIdx: Integer read FActiveIdx write FActiveIdx;
property OnActiveChanged: TChannelActiveEvent read FOnActiveChanged write FOnActiveChanged;
end;
implementation
constructor TChannelController.Create(ARadio: TRadioController);
begin
inherited Create;
FRadio := ARadio;
FActiveIdx := -1;
FPreChannelPower := -1;
end;
procedure TChannelController.EmitActive(Active: Boolean; const Name: string);
begin
if Assigned(FOnActiveChanged) then FOnActiveChanged(FActiveIdx, Name, Active);
end;
procedure TChannelController.SetDriveGuarded(V: Integer);
// Drive, выставляемый самим юнитом (применение мощности канала / восстановление):
// rfDrive прилетит, но OnDriveChanged НЕ должен трактовать его как ручной → не
// сбрасывать FPreChannelPower.
begin
FRestoringDrive := True;
try
FRadio.SetDrive(V);
finally
FRestoringDrive := False;
end;
end;
procedure TChannelController.OnDriveChanged;
// Вызывается из OnControllerState(rfDrive). Любое ВНЕШНЕЕ изменение drive
// (слайдер/web/энкодер) снимает сохранённую «до канала» мощность.
begin
if not FRestoringDrive then FPreChannelPower := -1;
end;
procedure TChannelController.Select(Idx: Integer);
var Ch: TChannel;
begin
if (FRadio.FChannelStore = nil) or (Idx < 0) or (Idx >= FRadio.FChannelStore.Count) then Exit;
Ch := FRadio.FChannelStore.GetChannel(Idx);
FActiveIdx := Idx;
EmitActive(True, Ch.Name); // рендер кнопки Channel (подсветка + имя)
Apply(Ch);
end;
procedure TChannelController.Apply(const Ch: TChannel);
var
i, NewBand, XvtrIdx: Integer;
begin
// Промежуточные retune внутри Apply (XVTR-выход → RestoreBand → SetVfoA на
// СТАРУЮ частоту) фаерят OnAfterTune→OnVfoTuned; без этого гарда CheckActive
// деактивировал бы канал прямо посреди его применения. Финальное состояние
// (FActiveIdx + Ch.RXFreq) выставляем сами — оркестрация снаружи Apply.
FApplying := True;
try
// Сохраняем текущее состояние до любых изменений
FRadio.SaveCurrentBand;
// Ищем подходящий XVTR-слот по частоте канала
XvtrIdx := -1;
for i := 0 to CFG_XVTR_COUNT - 1 do
if FRadio.FXvtrSettings.Entries[i].Enabled and
(Ch.RXFreq >= FRadio.FXvtrSettings.Entries[i].FreqBegin) and
(Ch.RXFreq <= FRadio.FXvtrSettings.Entries[i].FreqEnd) then
begin
XvtrIdx := i;
Break;
end;
NewBand := -1;
if XvtrIdx >= 0 then
begin
// Канал на трансвертерном диапазоне — активируем XVTR если не активен
if FRadio.FCurrentXvtr <> XvtrIdx then
FRadio.SetXvtrBand(XvtrIdx); // вход в слот + рендер/wideband/web (rfXvtr)
end
else
begin
// Канал на HF (или неизвестная частота). Если были на XVTR — выходим в HF,
// иначе SetVfoA заклэмпит частоту.
if FRadio.FCurrentXvtr >= 0 then
FRadio.SetXvtrBand(-1); // выход в HF (DeactivateXvtr несёт HF-возврат)
NewBand := FreqToBandIdx(Ch.RXFreq);
if (NewBand >= 0) and (NewBand <> FRadio.FCurrentBand) then
FRadio.SetCurrentBandIdx(NewBand); // band-индекс + drive-кал + рендер кнопок
end;
// Режим (после XVTR/Band режим мог измениться — сверяем снова). SetMode несёт
// DSP + дефолтный фильтр под режим + рендер (FM-панели для FM).
if FRadio.FMode <> Ch.Mode then
FRadio.SetMode(Ch.Mode);
// FM-параметры
if FRadio.FMode = MODE_FM then
begin
FRadio.FFMCTCSSAutoActive := Ch.CTCSSOn; // CTCSS из канала — авто, сбросится при уходе
FRadio.SetFMCTCSSTone(Ch.CTCSSToneIdx);
FRadio.SetFMCTCSS(Ch.CTCSSOn);
FRadio.FFMRptOffsetHz := Ch.RptOffsetHz; // зафиксировать до рендера в SetFMRpt
FRadio.SetFMRpt(Ch.RptDir); // dir + рендер кнопок/оффсета
FRadio.FFMRptAutoActive := Ch.RptDir <> RPT_NONE; // RPT из канала — авто
end;
// Мощность из канала. Запоминаем drive «до канала» для восстановления при уходе
// (не перетираем при переходе канал→канал).
if Ch.Power >= 0 then
begin
if FPreChannelPower < 0 then
FPreChannelPower := FRadio.FDrivePercent;
SetDriveGuarded(EnsureRange(Ch.Power, 0, 100));
end;
// Частота VFO A (XVTR-клэмп, CTUN, band-detect, сеть — всё в SetVfoA)
FRadio.SetVfoA(Round(Ch.RXFreq));
// Сохраняем итог в band/XVTR-кэш
if (XvtrIdx >= 0) or (NewBand >= 0) then
FRadio.SaveCurrentBand;
finally
FApplying := False;
end;
end;
procedure TChannelController.CheckActive;
var Ch: TChannel;
begin
if FActiveIdx < 0 then Exit;
if (FRadio.FChannelStore = nil) or (FActiveIdx >= FRadio.FChannelStore.Count) then
begin
FActiveIdx := -1;
EmitActive(False, '');
Exit;
end;
Ch := FRadio.FChannelStore.GetChannel(FActiveIdx);
if Round(FRadio.FVfoA) <> Round(Ch.RXFreq) then
begin
FActiveIdx := -1;
EmitActive(False, '');
// Сбрасываем CTCSS если он был включён каналом (не вручную)
if FRadio.FFMCTCSSAutoActive then
begin
FRadio.FFMCTCSSAutoActive := False;
FRadio.SetFMCTCSS(False);
end;
// Сбрасываем RPT если он был включён каналом (не вручную), затем даём
// UpdateRptAutoState переоценить авто-MINUS для текущей частоты.
if FRadio.FFMRptAutoActive then
begin
FRadio.SetFMRpt(RPT_NONE); // dir NONE + рендер off (снимает авто-флаг)
FRadio.UpdateRptAutoState;
end;
// Восстанавливаем мощность которая была до применения канала
if FPreChannelPower >= 0 then
begin
SetDriveGuarded(FPreChannelPower);
FPreChannelPower := -1;
end;
end;
end;
procedure TChannelController.OnVfoTuned;
// Вызывается из пути тюнинга (MainForm.ApplyVfoA) ПОСЛЕ FController.SetVfoA.
// Без активного канала: сброс авто-CTCSS + восстановление pre-channel drive.
// С активным каналом: CheckActive деактивирует при уходе с частоты канала.
begin
if FApplying then Exit; // промежуточные retune внутри Apply — не трогаем канал
if FActiveIdx < 0 then
begin
if FRadio.FFMCTCSSAutoActive then
begin
FRadio.FFMCTCSSAutoActive := False;
FRadio.SetFMCTCSS(False);
end;
if FPreChannelPower >= 0 then
begin
SetDriveGuarded(FPreChannelPower); // восстановить + push + рендер слайдера
FPreChannelPower := -1;
end;
end;
CheckActive; // no-op если FActiveIdx<0
end;
end.