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.