mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
- TRadioController.SetActiveVfo(Idx): switches the active RX VFO and retunes the receiver to its frequency. CTUN handling preserved: if the new VFO's freq is visible in the current window, only the demodulator shift changes (DDC/network untouched); otherwise CTUN is dropped and the window centers on the freq. HF band-detect + network push as before. Fires Changed(rfActiveVfo) (+ rfBand / rfCTun when those change). - OnControllerState(rfActiveVfo): UpdateVfoDisplay (freq displays + RX/TX button highlights) and a full spectrum/waterfall/ruler redraw. MainForm.ActivateVfo is now a thin delegate; all callers (VFO select/TX buttons, freq-display clicks, web ActivateVfo) go through it unchanged. Band-detect keeps the historical no-drive-recalc behavior. Builds clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1003 lines
38 KiB
ObjectPascal
1003 lines
38 KiB
ObjectPascal
unit RadioController;
|
||
|
||
{
|
||
EWSDR — Radio Controller (headless ядро)
|
||
|
||
Назначение
|
||
----------
|
||
TRadioController — «мозг» трансивера БЕЗ привязки к UI/LCL-формам. Владеет
|
||
состоянием радио и движками (сеть HPSDR, WDSP, аудио, настройки, каналы) и
|
||
предоставляет единый интерфейс управления для ЛЮБОГО фронтенда: GUI, Web, CAT,
|
||
аппаратная панель Arduino.
|
||
|
||
• Команды (SetMode, SetVfoA, TuneActiveBy, StartStop, …) — единая точка входа.
|
||
Абсолютные (Set*) удобны UI/Web; относительные (*By/Up/Down) — энкодерам.
|
||
• GetSnapshot → TRadioSnapshot — весь стейт одной записью (синхронизация
|
||
нового фронтенда при подключении: LED/дисплеи панели, веб-клиент).
|
||
• OnStateChanged(Field) — гранулярное уведомление об изменении поля; все
|
||
фронтенды остаются синхронны, кто бы ни инициировал изменение.
|
||
|
||
Маршалинг потоков
|
||
-----------------
|
||
Команды из сетевых/serial/web-потоков идут через OnInvoke (GUI = обёртка над
|
||
TThread.Synchronize; демон = своя очередь/прямой вызов), чтобы вызовы движка
|
||
шли из одного потока без зависимости от GUI-цикла LCL.
|
||
|
||
СТАТУС МИГРАЦИИ (Фаза 1)
|
||
------------------------
|
||
Состояние и движки вынесены сюда как ПУБЛИЧНЫЕ ПОЛЯ с теми же именами, что были
|
||
в TMainForm (FVfoA, FNetwork, FDSPEngine, …), чтобы проводка MainForm свелась к
|
||
механической замене FXxx -> FController.FXxx без изменения поведения. Логика
|
||
операций (ApplyModeFilter, BtnStartStopClick, ApplyVfoA, …) пока остаётся в
|
||
MainForm; команды контроллера обновляют поле + шлют событие и будут наполнены
|
||
логикой на следующих фазах (помечено TODO-wiring). Поля станут приватными,
|
||
когда MainForm перестанет писать в них напрямую.
|
||
|
||
ВЛАДЕНИЕ ДВИЖКАМИ (Фаза 1): объекты создаёт и освобождает MainForm (в
|
||
FormCreate/FormDestroy), сохраняя в поля контроллера. Перенос создания в
|
||
Create/Destroy контроллера — отдельная фаза (нужен для режима демона).
|
||
|
||
FPC 3.2+ / режим Delphi. Без inline var и анонимных процедур.
|
||
}
|
||
|
||
{$IFDEF FPC}
|
||
{$MODE Delphi}
|
||
{$ENDIF}
|
||
|
||
interface
|
||
|
||
uses
|
||
Classes, SysUtils, Math,
|
||
HPSDRProtocol, HPSDRNetwork,
|
||
WDSPEngine, AudioOutput, AudioInput,
|
||
Settings, ChannelStore, FMRepeater, BoardUtils;
|
||
|
||
type
|
||
// Поле, которое изменилось — для гранулярных уведомлений наружу.
|
||
TRadioField = (
|
||
rfVfoA, rfVfoB, rfActiveVfo,
|
||
rfMode, rfFilter, rfFilterBW,
|
||
rfAGCMode, rfAGCTop,
|
||
rfVolume, rfMute, rfDrive, rfAtten,
|
||
rfBand, rfXvtr,
|
||
rfCenterFreq, rfSpan, rfSampleRate,
|
||
rfCTun, rfDuplex,
|
||
rfRunning, rfConnected, rfTransmitting, rfTuning,
|
||
rfNR, rfNB, rfSNB, rfANF,
|
||
rfWfAGC, rfWfNF, rfDither, rfRandom,
|
||
rfFMDeviation, rfFMCTCSS, rfFMCTCSSTone, rfFMSQ, rfFMSQLevel,
|
||
rfFMStep, rfFMStepIdx, rfFMRpt,
|
||
rfChannel,
|
||
rfSMeter, rfPower, rfSWR, rfSupply, rfPLL, // телеметрия
|
||
rfDevice // подключённое устройство сменилось
|
||
);
|
||
|
||
TRadioStateEvent = procedure(Sender: TObject; Field: TRadioField) of object;
|
||
|
||
// Маршалинг произвольного метода в поток контроллера (GUI: TThread.Synchronize).
|
||
TUIInvokeEvent = procedure(M: TThreadMethod) of object;
|
||
|
||
// Снимок состояния для синхронизации нового фронтенда (Arduino/Web на connect).
|
||
TRadioSnapshot = record
|
||
VfoA, VfoB, CenterFreq, SpanHz: Double;
|
||
SampleRate: Integer;
|
||
ActiveVfo: Integer;
|
||
Mode, Filter, FilterBW: Integer;
|
||
AGCMode, AGCTop: Integer;
|
||
Volume, Drive, Atten: Integer;
|
||
Band, Xvtr: Integer;
|
||
Muted, CTun, Duplex: Boolean;
|
||
Running, Connected, Transmitting, Tuning: Boolean;
|
||
NRMode, NBMode: Integer;
|
||
SNB, ANF: Boolean;
|
||
WfAGC, WfNF: Boolean;
|
||
FMStepOn: Boolean;
|
||
FMStepIdx: Integer;
|
||
FMCTCSSOn: Boolean;
|
||
FMCTCSSToneIdx: Integer;
|
||
FMSQOn: Boolean;
|
||
FMSQLevel: Integer;
|
||
FMRptDir: Integer;
|
||
SMeter, FwdW, SWR, SupplyV, SupplyA: Double;
|
||
PLLLock: Boolean;
|
||
end;
|
||
|
||
{ TRadioController }
|
||
TRadioController = class
|
||
private
|
||
FOnInvoke: TUIInvokeEvent;
|
||
FOnStateChanged: TRadioStateEvent;
|
||
procedure Changed(Field: TRadioField);
|
||
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
|
||
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
|
||
function ActiveVfoHz: Double; // частота активного VFO (A или B)
|
||
procedure ApplyTuneCore(VfoHz: Double); // CTUN shift/center/scroll для актив. VFO
|
||
public
|
||
// ============================================================
|
||
// ПУБЛИЧНЫЕ ПОЛЯ (миграционные — имена совпадают с TMainForm).
|
||
// Внешний чистый доступ для адаптеров — через команды / GetSnapshot /
|
||
// OnStateChanged. Прямая запись извне — временная мера на время Фазы 1.
|
||
// ============================================================
|
||
|
||
// ---- Движки (владение: см. шапку юнита) ----
|
||
FNetwork: THPSDRNetwork;
|
||
FDSPEngine: TWDSPEngine;
|
||
FAudioOut: TAudioOutput;
|
||
FAudioIn: TAudioInput;
|
||
FSettings: TSettingsManager;
|
||
FChannelStore: TChannelStore;
|
||
FWDSPReady: Boolean;
|
||
|
||
// ---- Частоты / режим / фильтр / AGC ----
|
||
FVfoA, FVfoB: Double;
|
||
FActiveVfo: Integer;
|
||
FMode: Integer;
|
||
FFilter: Integer;
|
||
FFilterBW: Integer;
|
||
FAGCMode: Integer;
|
||
FAGCTop: Integer;
|
||
FCTun: Boolean;
|
||
FCenterFreq: Double;
|
||
FSpanHz: Double;
|
||
FSampleRate: Integer;
|
||
FCurrentBand: Integer;
|
||
FCurrentXvtr: Integer;
|
||
|
||
// ---- Аудио / TX ----
|
||
FVolume: Integer;
|
||
FMuted: Boolean;
|
||
FDrivePercent: Integer; // драйв 0..100 % (UI-уровень, источник истины)
|
||
FDriveLevel: Byte; // вычисляемый hardware-байт (CalcDriveByte)
|
||
FLastVfoScrolled: Boolean; // последний SetVfoA прокрутил центр (для водопада)
|
||
FAtten: Integer;
|
||
FPAMaxPower: Double;
|
||
FRunning: Boolean;
|
||
FTransmitting: Boolean;
|
||
FTuning: Boolean;
|
||
FSplitTxB: Boolean;
|
||
FDisplayDuplex: Boolean;
|
||
FHWPTTActive: Boolean;
|
||
FHWPTTStartedTX: Boolean;
|
||
FSendAudioToRadio: Boolean;
|
||
FAudioOutDevName: string;
|
||
FAudioInDevName: string;
|
||
FDevConnected: Boolean;
|
||
|
||
// ---- DSP-фильтры шума / водопад ----
|
||
FNRMode, FNBMode: Integer; // источник истины (раньше Btn*.Tag)
|
||
FSNB, FANF: Boolean; // источник истины (раньше Btn*.Tag)
|
||
FWfAGCEnabled: Boolean;
|
||
FWfNFEnabled: Boolean;
|
||
FDitherEnabled: Boolean;
|
||
FRandomEnabled: Boolean;
|
||
|
||
// ---- FM ----
|
||
FFMDeviation: Double;
|
||
FFMCTCSSOn: Boolean;
|
||
FFMCTCSSToneIdx: Integer;
|
||
FFMSQOn: Boolean;
|
||
FFMSQLevel: Integer;
|
||
FFMStepOn: Boolean;
|
||
FFMStepIdx: Integer;
|
||
FFMRptDir: Integer;
|
||
FFMRptOffsetHz: Double;
|
||
FFMRptAutoActive: Boolean;
|
||
FFMCTCSSAutoActive: Boolean;
|
||
|
||
// ---- Конфиги / устройство (per-MAC) ----
|
||
FTXSettings: TTXSettings;
|
||
FAlexSettings: TAlexSettings;
|
||
FXvtrSettings: TXvtrSettings;
|
||
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings;
|
||
FPABandCal: array[0..CFG_BAND_COUNT-1] of Double;
|
||
FVHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double;
|
||
FDevMAC: array[0..5] of Byte;
|
||
|
||
// ---- Телеметрия (наполняется в Фазе 3/4 вместе с сетевыми колбэками) ----
|
||
FLastSMeter, FLastFwdW, FLastSWR, FLastSupplyV, FLastSupplyA: Double;
|
||
FLastPLLLock: Boolean;
|
||
|
||
constructor Create;
|
||
destructor Destroy; override;
|
||
|
||
// Маршалинг команды в поток контроллера (GUI: TThread.Synchronize).
|
||
procedure Invoke(M: TThreadMethod);
|
||
|
||
// Снимок всего состояния (для синхронизации нового фронтенда).
|
||
function GetSnapshot: TRadioSnapshot;
|
||
|
||
// Частотные хелперы — чистые вычисления над состоянием, общие для всех
|
||
// фронтендов и сетевого слоя (Vfo/Mode/Band/CTun/TX/FM-Rpt).
|
||
// XvtrTranslate — переводит видимую частоту в частоту IF-тракта с учётом
|
||
// активного трансвертора (LOOffset/LOError).
|
||
// ActiveTXFreqHz — текущая TX-частота (split/active VFO + FM-репитер сдвиг).
|
||
function XvtrTranslate(VisibleHz: Double): Double;
|
||
function ActiveTXFreqHz: Double;
|
||
|
||
// Применяет текущие FMode/FFilterBW/FM-параметры к DSP (RX/TX фильтр,
|
||
// FM-девиация, squelch). Вызывается командами и UI-путями смены режима.
|
||
procedure ApplyModeFilter;
|
||
|
||
// Вычисляет hardware drive-байт (Thetis-формула) из текущего состояния
|
||
// (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием.
|
||
function CalcDriveByte: Byte;
|
||
|
||
// Полный HP-кадр в радио (RX/TX freq через трансвертор + drive + MOX).
|
||
// Вызывается командами смены частоты/центра и UI после правки drive.
|
||
procedure PushNetworkState;
|
||
|
||
// ====================================================================
|
||
// КОМАНДЫ — единый API для GUI / Web / CAT / Arduino.
|
||
// Фаза 1: обновляют поле + шлют событие; логика движка — TODO-wiring.
|
||
// ====================================================================
|
||
|
||
// Частоты / VFO
|
||
procedure SetVfoA(Hz: Double);
|
||
procedure SetVfoB(Hz: Double);
|
||
procedure SetActiveVfo(Idx: Integer);
|
||
procedure TuneActiveBy(DeltaHz: Double); // энкодер VFO
|
||
procedure SetCenter(Hz: Double);
|
||
procedure VfoSwap;
|
||
procedure VfoACopyB;
|
||
procedure VfoBCopyA;
|
||
|
||
// Режим / фильтр / AGC
|
||
procedure SetMode(M: Integer);
|
||
procedure SetFilterIdx(Idx: Integer);
|
||
procedure SetFilterBW(BWHz: Integer);
|
||
procedure SetAGCMode(M: Integer);
|
||
procedure SetAGCTop(DB: Integer);
|
||
procedure AGCTopBy(Delta: Integer);
|
||
|
||
// Громкость / mute / drive / аттенюатор
|
||
procedure SetVolume(V: Integer);
|
||
procedure VolumeBy(Delta: Integer);
|
||
procedure SetMute(On_: Boolean);
|
||
procedure ToggleMute;
|
||
procedure SetDrive(V: Integer);
|
||
procedure DriveBy(Delta: Integer);
|
||
procedure SetAtten(Idx: Integer);
|
||
|
||
// Диапазоны / трансвертеры / спан
|
||
procedure SetBand(Idx: Integer);
|
||
procedure BandUp;
|
||
procedure BandDown;
|
||
procedure SetXvtrBand(Idx: Integer); // -1 = выход в HF
|
||
procedure SetSpan(Hz: Integer);
|
||
procedure SetSampleRate(Hz: Integer);
|
||
|
||
// Отображение / приём
|
||
procedure SetCTun(On_: Boolean);
|
||
procedure SetDuplex(On_: Boolean);
|
||
procedure SetWfAGC(On_: Boolean);
|
||
procedure SetWfNF(On_: Boolean);
|
||
|
||
// DSP-фильтры шума
|
||
procedure SetNR(Mode: Integer);
|
||
procedure SetNB(Mode: Integer);
|
||
procedure SetSNB(On_: Boolean);
|
||
procedure SetANF(On_: Boolean);
|
||
|
||
// Передача
|
||
procedure SetMOX(On_: Boolean);
|
||
procedure SetTune(On_: Boolean); // TUN
|
||
|
||
// FM
|
||
procedure SetFMStep(Idx: Integer);
|
||
procedure SetFMCTCSS(On_: Boolean);
|
||
procedure SetFMCTCSSTone(Idx: Integer);
|
||
procedure SetFMSquelch(On_: Boolean);
|
||
procedure SetFMSquelchLevel(V: Integer);
|
||
procedure SetFMRpt(Dir: Integer); // явное направление; снимает авто-флаг
|
||
procedure SetFMRptOffset(Hz: Double); // сдвиг репитера; применяет, если RPT вкл
|
||
procedure UpdateRptAutoState; // авто-MINUS для 2 м под FVfoA
|
||
|
||
// Каналы
|
||
procedure ApplyChannel(Idx: Integer);
|
||
|
||
// Жизненный цикл движка / устройства
|
||
procedure StartStop;
|
||
procedure SetRun(On_: Boolean);
|
||
procedure ConnectDevice(const Dev: THPSDRDevice);
|
||
procedure Discover;
|
||
|
||
// ---- События наружу ----
|
||
property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke;
|
||
property OnStateChanged: TRadioStateEvent read FOnStateChanged write FOnStateChanged;
|
||
end;
|
||
|
||
implementation
|
||
|
||
constructor TRadioController.Create;
|
||
begin
|
||
inherited Create;
|
||
// Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны).
|
||
FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0;
|
||
FMode := 1; FFilter := 5; FFilterBW := 2700;
|
||
FAGCMode := 1; FAGCTop := 90;
|
||
FVolume := 70; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
|
||
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
|
||
FCurrentBand := 5; FCurrentXvtr := -1;
|
||
FPAMaxPower := 100.0;
|
||
FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True;
|
||
FFMStepOn := True;
|
||
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
|
||
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
|
||
end;
|
||
|
||
destructor TRadioController.Destroy;
|
||
begin
|
||
// Фаза 1: движки освобождает MainForm. Здесь ничего не освобождаем.
|
||
inherited Destroy;
|
||
end;
|
||
|
||
procedure TRadioController.Changed(Field: TRadioField);
|
||
begin
|
||
if Assigned(FOnStateChanged) then FOnStateChanged(Self, Field);
|
||
end;
|
||
|
||
procedure TRadioController.PushNetworkState;
|
||
// Полный HP-кадр в радио: RX/TX частоты (через трансвертор), drive, состояние
|
||
// MOX. Вызывается командами, которые меняют центр/частоту/репитер-сдвиг.
|
||
begin
|
||
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
|
||
FDriveLevel, FTransmitting, True, True);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.Invoke(M: TThreadMethod);
|
||
begin
|
||
if Assigned(FOnInvoke) then FOnInvoke(M)
|
||
else if Assigned(M) then M(); // нет хозяина-маршалера — прямой вызов
|
||
end;
|
||
|
||
function TRadioController.GetSnapshot: TRadioSnapshot;
|
||
begin
|
||
FillChar(Result, SizeOf(Result), 0);
|
||
Result.VfoA := FVfoA; Result.VfoB := FVfoB;
|
||
Result.CenterFreq := FCenterFreq; Result.SpanHz := FSpanHz;
|
||
Result.SampleRate := FSampleRate; Result.ActiveVfo := FActiveVfo;
|
||
Result.Mode := FMode; Result.Filter := FFilter; Result.FilterBW := FFilterBW;
|
||
Result.AGCMode := FAGCMode; Result.AGCTop := FAGCTop;
|
||
Result.Volume := FVolume; Result.Drive := FDrivePercent; Result.Atten := FAtten;
|
||
Result.Band := FCurrentBand; Result.Xvtr := FCurrentXvtr;
|
||
Result.Muted := FMuted; Result.CTun := FCTun; Result.Duplex := FDisplayDuplex;
|
||
Result.Running := FRunning; Result.Connected := FDevConnected;
|
||
Result.Transmitting := FTransmitting; Result.Tuning := FTuning;
|
||
Result.NRMode := FNRMode; Result.NBMode := FNBMode;
|
||
Result.SNB := FSNB; Result.ANF := FANF;
|
||
Result.WfAGC := FWfAGCEnabled; Result.WfNF := FWfNFEnabled;
|
||
Result.FMStepOn := FFMStepOn; Result.FMStepIdx := FFMStepIdx;
|
||
Result.FMCTCSSOn := FFMCTCSSOn; Result.FMCTCSSToneIdx := FFMCTCSSToneIdx;
|
||
Result.FMSQOn := FFMSQOn; Result.FMSQLevel := FFMSQLevel;
|
||
Result.FMRptDir := FFMRptDir;
|
||
Result.SMeter := FLastSMeter; Result.FwdW := FLastFwdW; Result.SWR := FLastSWR;
|
||
Result.SupplyV := FLastSupplyV; Result.SupplyA := FLastSupplyA;
|
||
Result.PLLLock := FLastPLLLock;
|
||
end;
|
||
|
||
function TRadioController.XvtrTranslate(VisibleHz: Double): Double;
|
||
begin
|
||
Result := VisibleHz;
|
||
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
|
||
if not FXvtrSettings.Entries[FCurrentXvtr].Enabled then Exit;
|
||
// f_IF = f_visible - LOOffset + LOError
|
||
Result := VisibleHz
|
||
- FXvtrSettings.Entries[FCurrentXvtr].LOOffset
|
||
+ FXvtrSettings.Entries[FCurrentXvtr].LOError;
|
||
end;
|
||
|
||
function TRadioController.ActiveTXFreqHz: Double;
|
||
begin
|
||
// TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B),
|
||
// независимо от активного RX-VFO. Раньше при не-split TX ошибочно следовал
|
||
// за активным RX (TX-A горит, но передача шла на B при активном B).
|
||
if FSplitTxB then Result := FVfoB
|
||
else Result := FVfoA;
|
||
if FMode = MODE_FM then
|
||
case FFMRptDir of
|
||
RPT_MINUS: Result := Result - FFMRptOffsetHz;
|
||
RPT_PLUS: Result := Result + FFMRptOffsetHz;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.ApplyModeFilter;
|
||
var
|
||
Lo, Hi, Half: Integer;
|
||
begin
|
||
Half := FFilterBW div 2;
|
||
case FMode of
|
||
0: begin Lo := -FFilterBW; Hi := -100; end; // LSB
|
||
1: begin Lo := 100; Hi := FFilterBW; end; // USB
|
||
else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM
|
||
end;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetFilter(Lo, Hi);
|
||
FDSPEngine.SetTXFilter(Lo, Hi);
|
||
if FMode = MODE_FM then
|
||
begin
|
||
FDSPEngine.SetRXFMDeviation(FFMDeviation);
|
||
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut,
|
||
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
|
||
FDSPEngine.SetFMSquelch(FFMSQOn, FFMSQLevel);
|
||
end
|
||
else
|
||
FDSPEngine.SetFMSquelch(False, 0);
|
||
end;
|
||
end;
|
||
|
||
// Выбирает дефолтный фильтр (индекс + полоса + FM-девиация) под текущий FMode.
|
||
// Зеркалит state-часть UI UpdateFilterButtons, чтобы смена режима была
|
||
// корректной и без UI (headless). FM сохраняет текущий NFM/WFM, прочие — дефолт.
|
||
procedure TRadioController.ApplyModeDefaults;
|
||
begin
|
||
case FMode of
|
||
MODE_FM:
|
||
begin
|
||
if FFilter > 1 then FFilter := FILT_FM_DEF;
|
||
FFilterBW := FILT_FM_BW[FFilter];
|
||
FFMDeviation := FILT_FM_DEV[FFilter];
|
||
end;
|
||
0, 1: begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
|
||
2: begin FFilter := FILT_DSB_DEF; FFilterBW := FILT_DSB_BW[FFilter]; end;
|
||
3, 4: begin FFilter := FILT_CW_DEF; FFilterBW := FILT_CW_BW[FFilter]; end;
|
||
else begin FFilter := FILT_AM_DEF; FFilterBW := FILT_AM_BW[FFilter]; end;
|
||
end;
|
||
end;
|
||
|
||
// =====================================================================
|
||
// Команды (Фаза 1 — каркас). Полная логика переносится из TMainForm.
|
||
// =====================================================================
|
||
|
||
function TRadioController.CalcDriveByte: Byte;
|
||
// Thetis-совместимая формула:
|
||
// drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 )
|
||
// target_volts = sqrt( 10^(target_dBm/10) * 0.05 )
|
||
// target_dBm = 10*log10(pwr_W*1000) - Cal ; pwr_W = Pos/100 * FPAMaxPower
|
||
var
|
||
Cal: Double;
|
||
Pos: Integer;
|
||
XvtrActive: Boolean;
|
||
pwr_W: Double;
|
||
target_dBm: Double;
|
||
target_volts: Double;
|
||
audio_vol: Double;
|
||
begin
|
||
XvtrActive := (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FXvtrSettings.Entries[FCurrentXvtr].Enabled;
|
||
if XvtrActive then
|
||
Cal := FVHFBandCal[FCurrentXvtr]
|
||
else if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
|
||
Cal := FPABandCal[FCurrentBand]
|
||
else
|
||
Cal := 38.8;
|
||
|
||
if FTuning then
|
||
begin
|
||
if XvtrActive and FXvtrSettings.UseXVTRTunePower then
|
||
Pos := EnsureRange(FXvtrSettings.Entries[FCurrentXvtr].TXPower, 0, 100)
|
||
else
|
||
Pos := EnsureRange(FTXSettings.TUNLevel, 0, 100);
|
||
end
|
||
else
|
||
begin
|
||
if XvtrActive then
|
||
Pos := Round(FDrivePercent * FXvtrSettings.Entries[FCurrentXvtr].TXPower / 100.0)
|
||
else
|
||
Pos := FDrivePercent;
|
||
end;
|
||
|
||
if Pos <= 0 then begin Result := 0; Exit; end;
|
||
|
||
pwr_W := Pos / 100.0 * FPAMaxPower;
|
||
target_dBm := 10.0 * Log10(pwr_W * 1000.0) - Cal;
|
||
target_volts := Sqrt(Power(10.0, target_dBm * 0.1) * 0.05);
|
||
audio_vol := Min(target_volts / 0.8, 1.0);
|
||
Result := Min(Round(audio_vol * 1.02 * 255.0), 255);
|
||
end;
|
||
|
||
function TRadioController.ActiveVfoHz: Double;
|
||
begin
|
||
if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB;
|
||
end;
|
||
|
||
procedure TRadioController.ApplyTuneCore(VfoHz: Double);
|
||
// Общее ядро перестройки приёмника на частоту АКТИВНОГО VFO (A или B).
|
||
// CTUN OFF: DDC = VfoHz, сдвиг 0.
|
||
// CTUN ON: DDC фиксирован, демодулятор сдвигаем на VfoHz-FCenterFreq;
|
||
// если полоса фильтра вышла за край дисплея — прокручиваем центр.
|
||
// Ставит FCenterFreq / shift / FLastVfoScrolled.
|
||
var
|
||
Offset, FiltLo, FiltHi, HalfSpan: Double;
|
||
begin
|
||
FLastVfoScrolled := False;
|
||
if not FCTun then
|
||
begin
|
||
FCenterFreq := VfoHz;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
||
end
|
||
else
|
||
begin
|
||
Offset := VfoHz - FCenterFreq;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset);
|
||
HalfSpan := FSpanHz / 2;
|
||
case FMode of
|
||
0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
|
||
1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end;
|
||
else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end;
|
||
end;
|
||
if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then
|
||
begin
|
||
if FiltHi > HalfSpan then
|
||
FCenterFreq := VfoHz - HalfSpan * 0.5
|
||
else
|
||
FCenterFreq := VfoHz + HalfSpan * 0.5;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(VfoHz - FCenterFreq);
|
||
FLastVfoScrolled := True;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.SetVfoA(Hz: Double);
|
||
// Перестройка VFO A: XVTR-клэмп, CTUN ядро, детект диапазона, FM авто-RPT,
|
||
// проталкивание в сеть. Рендер — в UI через OnControllerState(rfVfoA/rfBand).
|
||
// Канальную оркестрацию (авто-CTCSS, pre-channel drive) делает UI-путь.
|
||
var
|
||
NewFreq: Int64;
|
||
BandIdx: Integer;
|
||
begin
|
||
NewFreq := Round(Hz);
|
||
// Клэмп в пределах XVTR-band — не даём VFO уйти за FreqBegin/FreqEnd
|
||
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;
|
||
FVfoA := NewFreq;
|
||
ApplyTuneCore(FVfoA);
|
||
|
||
// Детект диапазона (только HF: в XVTR клэмп держит VFO внутри band,
|
||
// FreqToBandIdx вернёт -1 для VHF/UHF).
|
||
if FCurrentXvtr < 0 then
|
||
begin
|
||
BandIdx := FreqToBandIdx(FVfoA);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
FCurrentBand := BandIdx;
|
||
FDriveLevel := CalcDriveByte;
|
||
Changed(rfBand);
|
||
end;
|
||
end;
|
||
|
||
// Авто-MINUS в 2 м репитерном сегменте (само снимает при уходе из сегмента).
|
||
if FMode = MODE_FM then UpdateRptAutoState;
|
||
|
||
PushNetworkState;
|
||
Changed(rfVfoA);
|
||
end;
|
||
|
||
procedure TRadioController.SetVfoB(Hz: Double);
|
||
// VFO B всегда сохраняется. Если B активен — перестраиваем приёмник через
|
||
// ApplyTuneCore (с учётом CTUN), детектим диапазон и толкаем в сеть. Если B
|
||
// неактивен (split, слушаем A) — только сохраняем частоту (для TX).
|
||
// Рендер FreqDispB/спектра — в UI через OnControllerState(rfVfoB).
|
||
var BandIdx: Integer;
|
||
begin
|
||
FVfoB := Hz;
|
||
if FActiveVfo = 1 then
|
||
begin
|
||
ApplyTuneCore(FVfoB);
|
||
if FCurrentXvtr < 0 then
|
||
begin
|
||
BandIdx := FreqToBandIdx(FVfoB);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
FCurrentBand := BandIdx;
|
||
FDriveLevel := CalcDriveByte;
|
||
Changed(rfBand);
|
||
end;
|
||
end;
|
||
PushNetworkState;
|
||
end;
|
||
Changed(rfVfoB);
|
||
end;
|
||
|
||
procedure TRadioController.SetActiveVfo(Idx: Integer);
|
||
// Переключение активного (RX) VFO с перестройкой приёмника на его частоту.
|
||
// CTUN: если частота нового VFO видна в текущем окне — только меняем shift
|
||
// (DDC/сеть не трогаем); иначе CTUN сбрасывается и центрируемся на частоте.
|
||
// Рендер (дисплеи/спектр/band/CTUN-кнопка) — в UI через OnControllerState.
|
||
var
|
||
ActiveFreq: Double;
|
||
HalfSpan, Offset: Double;
|
||
FreqVisible: Boolean;
|
||
BandIdx: Integer;
|
||
begin
|
||
FActiveVfo := Idx;
|
||
ActiveFreq := ActiveVfoHz;
|
||
|
||
if FCTun then
|
||
begin
|
||
HalfSpan := FSpanHz / 2;
|
||
Offset := ActiveFreq - FCenterFreq;
|
||
FreqVisible := (Offset > -HalfSpan) and (Offset < HalfSpan);
|
||
if FreqVisible then
|
||
begin
|
||
// Частота видна — просто сдвигаем демодулятор, DDC/сеть не трогаем.
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset);
|
||
end
|
||
else
|
||
begin
|
||
// Частота за окном — сбрасываем CTUN и центрируемся на ней.
|
||
FCTun := False;
|
||
FBandCache[FCurrentBand].CTun := False;
|
||
FCenterFreq := ActiveFreq;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
||
PushNetworkState;
|
||
Changed(rfCTun);
|
||
end;
|
||
end
|
||
else
|
||
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);
|
||
end;
|
||
|
||
Changed(rfActiveVfo);
|
||
end;
|
||
|
||
procedure TRadioController.TuneActiveBy(DeltaHz: Double);
|
||
begin
|
||
if FActiveVfo = 0 then SetVfoA(FVfoA + DeltaHz)
|
||
else SetVfoB(FVfoB + DeltaHz);
|
||
end;
|
||
|
||
procedure TRadioController.SetCenter(Hz: Double);
|
||
begin FCenterFreq := Hz; Changed(rfCenterFreq); { TODO wiring } end;
|
||
|
||
procedure TRadioController.VfoSwap;
|
||
var T: Double;
|
||
begin T := FVfoA; FVfoA := FVfoB; FVfoB := T; Changed(rfVfoA); Changed(rfVfoB); { TODO wiring } end;
|
||
|
||
procedure TRadioController.VfoACopyB;
|
||
begin FVfoA := FVfoB; Changed(rfVfoA); { TODO wiring } end;
|
||
|
||
procedure TRadioController.VfoBCopyA;
|
||
begin FVfoB := FVfoA; Changed(rfVfoB); { TODO wiring } end;
|
||
|
||
procedure TRadioController.SetMode(M: Integer);
|
||
begin
|
||
FMode := M;
|
||
ApplyModeDefaults; // дефолтный фильтр/девиация под режим
|
||
FBandCache[FCurrentBand].Mode := FMode;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetMode(FMode);
|
||
// Если TUN активен — пересчитываем знак частоты под новый режим
|
||
if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
|
||
end;
|
||
ApplyModeFilter;
|
||
Changed(rfMode);
|
||
end;
|
||
|
||
procedure TRadioController.SetFilterIdx(Idx: Integer);
|
||
begin
|
||
FFilter := Idx;
|
||
case FMode of
|
||
0, 1: FFilterBW := FILT_SSB_BW[Idx];
|
||
2: FFilterBW := FILT_DSB_BW[Idx];
|
||
3, 4: FFilterBW := FILT_CW_BW[Idx];
|
||
MODE_FM:
|
||
begin
|
||
FFilterBW := FILT_FM_BW[Idx];
|
||
FFMDeviation := FILT_FM_DEV[Idx];
|
||
end;
|
||
else FFilterBW := FILT_AM_BW[Idx];
|
||
end;
|
||
FBandCache[FCurrentBand].FilterBW := FFilterBW;
|
||
ApplyModeFilter;
|
||
Changed(rfFilter);
|
||
end;
|
||
|
||
procedure TRadioController.SetFilterBW(BWHz: Integer);
|
||
begin FFilterBW := BWHz; Changed(rfFilterBW); { TODO wiring } end;
|
||
|
||
procedure TRadioController.ApplyAGCToEngine;
|
||
const
|
||
// UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для
|
||
// всех фронтендов (раньше web/слайдер ошибочно кастовали индекс прямо в enum).
|
||
AGCModes: array[0..4] of TWDSPAGCMode = (agcFast, agcMedium, agcSlow, agcLong, agcOff);
|
||
begin
|
||
if FWDSPReady and Assigned(FDSPEngine) and (FAGCMode >= 0) and (FAGCMode <= 4) then
|
||
FDSPEngine.SetAGC(AGCModes[FAGCMode], 50.0);
|
||
end;
|
||
|
||
procedure TRadioController.SetAGCMode(M: Integer);
|
||
begin
|
||
if (M < 0) or (M > 4) then Exit;
|
||
FAGCMode := M;
|
||
ApplyAGCToEngine;
|
||
FBandCache[FCurrentBand].AGCMode := FAGCMode;
|
||
Changed(rfAGCMode);
|
||
end;
|
||
|
||
procedure TRadioController.SetAGCTop(DB: Integer);
|
||
begin
|
||
if DB < 20 then DB := 20 else if DB > 120 then DB := 120;
|
||
FAGCTop := DB;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetAGCTop(FAGCTop);
|
||
ApplyAGCToEngine; // ре-применяем режим с новым уровнем
|
||
FBandCache[FCurrentBand].AGCTop := FAGCTop;
|
||
Changed(rfAGCTop);
|
||
end;
|
||
|
||
procedure TRadioController.AGCTopBy(Delta: Integer);
|
||
begin SetAGCTop(FAGCTop + Delta); end;
|
||
|
||
procedure TRadioController.SetVolume(V: Integer);
|
||
begin
|
||
if V < 0 then V := 0 else if V > 100 then V := 100;
|
||
FVolume := V;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetVolume(FVolume / 100.0);
|
||
Changed(rfVolume);
|
||
end;
|
||
|
||
procedure TRadioController.VolumeBy(Delta: Integer);
|
||
begin SetVolume(FVolume + Delta); end;
|
||
|
||
procedure TRadioController.SetMute(On_: Boolean);
|
||
begin
|
||
FMuted := On_;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMute(FMuted);
|
||
Changed(rfMute);
|
||
end;
|
||
|
||
procedure TRadioController.ToggleMute;
|
||
begin SetMute(not FMuted); end;
|
||
|
||
procedure TRadioController.SetDrive(V: Integer);
|
||
begin FDrivePercent := V; Changed(rfDrive); { TODO wiring: CalcDriveByte + WDSP } end;
|
||
|
||
procedure TRadioController.DriveBy(Delta: Integer);
|
||
begin SetDrive(FDrivePercent + Delta); end;
|
||
|
||
procedure TRadioController.SetAtten(Idx: Integer);
|
||
begin
|
||
if Idx < 0 then Idx := 0 else if Idx > 2 then Idx := 2;
|
||
FAtten := Idx;
|
||
if Assigned(FNetwork) then FNetwork.SetStepAtten(FAtten * 10);
|
||
Changed(rfAtten);
|
||
end;
|
||
|
||
procedure TRadioController.SetBand(Idx: Integer);
|
||
begin FCurrentBand := Idx; Changed(rfBand); { TODO wiring: RestoreBand } end;
|
||
|
||
procedure TRadioController.BandUp;
|
||
begin if FCurrentBand < CFG_BAND_COUNT - 1 then SetBand(FCurrentBand + 1); end;
|
||
|
||
procedure TRadioController.BandDown;
|
||
begin if FCurrentBand > 0 then SetBand(FCurrentBand - 1); end;
|
||
|
||
procedure TRadioController.SetXvtrBand(Idx: Integer);
|
||
begin FCurrentXvtr := Idx; Changed(rfXvtr); { TODO wiring } end;
|
||
|
||
procedure TRadioController.SetSpan(Hz: Integer);
|
||
begin FSpanHz := Hz; Changed(rfSpan); { TODO wiring } end;
|
||
|
||
procedure TRadioController.SetSampleRate(Hz: Integer);
|
||
// Смена частоты дискретизации DDC. В этом приложении span ≡ sample rate.
|
||
// Несёт: пересоздание WDSP-канала (Close+FlushQueue+Open), новый rate в
|
||
// трансивер и полное восстановление DSP-состояния. Сохранение в global
|
||
// settings + рендер overlay/AGC-линий остаются в UI (OnControllerState).
|
||
begin
|
||
if (Hz <= 0) or (Hz = FSampleRate) then Exit;
|
||
FSampleRate := Hz;
|
||
FSpanHz := Hz;
|
||
FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости
|
||
|
||
// 1. Останавливаем WDSP и меняем rate (Close + FlushQueue + Open).
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
FWDSPReady := False;
|
||
FDSPEngine.ChangeSampleRate(Hz);
|
||
end;
|
||
|
||
// 2. Новый rate трансиверу (после остановки DSP — пакеты сразу с верным rate).
|
||
if Assigned(FNetwork) and FNetwork.Connected then
|
||
FNetwork.ConfigureDDCs(1, Hz div 1000, 0, FDitherEnabled, FRandomEnabled);
|
||
|
||
// 3. Восстанавливаем DSP-состояние (ChangeSampleRate пересоздал канал).
|
||
if Assigned(FDSPEngine) then
|
||
begin
|
||
FWDSPReady := FDSPEngine.Initialized;
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetMode(FMode);
|
||
FDSPEngine.SetVolume(FVolume / 100.0);
|
||
ApplyModeFilter;
|
||
FDSPEngine.SetNRMode(FNRMode);
|
||
FDSPEngine.SetNBMode(FNBMode);
|
||
FDSPEngine.SetSNB(FSNB);
|
||
FDSPEngine.SetANF(FANF);
|
||
FDSPEngine.SetAGCTop(FAGCTop);
|
||
ApplyAGCToEngine; // единый маппинг (раньше здесь был ошибочный каст индекса)
|
||
if Assigned(FNetwork) and FNetwork.Connected then
|
||
FDSPEngine.SetShift(FVfoA - FCenterFreq);
|
||
end;
|
||
end;
|
||
Changed(rfSampleRate);
|
||
end;
|
||
|
||
procedure TRadioController.SetCTun(On_: Boolean);
|
||
begin
|
||
FCTun := On_;
|
||
FBandCache[FCurrentBand].CTun := FCTun;
|
||
if not FCTun then
|
||
begin
|
||
// Выключили CTUN: центрируемся на АКТИВНОМ VFO, shift=0. (Включение CTUN не
|
||
// трогает движок — FCenterFreq остаётся как есть, спектр не прыгает.)
|
||
FCenterFreq := ActiveVfoHz;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
||
PushNetworkState;
|
||
end;
|
||
Changed(rfCTun);
|
||
end;
|
||
|
||
procedure TRadioController.SetDuplex(On_: Boolean);
|
||
begin FDisplayDuplex := On_; Changed(rfDuplex); { TODO wiring } end;
|
||
|
||
procedure TRadioController.SetWfAGC(On_: Boolean);
|
||
begin FWfAGCEnabled := On_; Changed(rfWfAGC); end;
|
||
|
||
procedure TRadioController.SetWfNF(On_: Boolean);
|
||
begin FWfNFEnabled := On_; Changed(rfWfNF); end;
|
||
|
||
procedure TRadioController.SetNR(Mode: Integer);
|
||
begin
|
||
if Mode < 0 then Mode := 0 else if Mode > 4 then Mode := 4;
|
||
FNRMode := Mode;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetNRMode(FNRMode);
|
||
Changed(rfNR);
|
||
end;
|
||
|
||
procedure TRadioController.SetNB(Mode: Integer);
|
||
begin
|
||
if Mode < 0 then Mode := 0 else if Mode > 2 then Mode := 2;
|
||
FNBMode := Mode;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetNBMode(FNBMode);
|
||
Changed(rfNB);
|
||
end;
|
||
|
||
procedure TRadioController.SetSNB(On_: Boolean);
|
||
begin
|
||
FSNB := On_;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetSNB(FSNB);
|
||
Changed(rfSNB);
|
||
end;
|
||
|
||
procedure TRadioController.SetANF(On_: Boolean);
|
||
begin
|
||
FANF := On_;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetANF(FANF);
|
||
Changed(rfANF);
|
||
end;
|
||
|
||
procedure TRadioController.SetMOX(On_: Boolean);
|
||
begin FTransmitting := On_; Changed(rfTransmitting); { TODO wiring: ApplyMOX } end;
|
||
|
||
procedure TRadioController.SetTune(On_: Boolean);
|
||
begin FTuning := On_; Changed(rfTuning); { TODO wiring: ApplyTUN } end;
|
||
|
||
procedure TRadioController.SetFMStep(Idx: Integer);
|
||
begin
|
||
if Idx < 0 then Idx := 0 else if Idx > 3 then Idx := 3; // FM_STEP_COUNT = 4
|
||
FFMStepIdx := Idx;
|
||
Changed(rfFMStepIdx);
|
||
end;
|
||
|
||
procedure TRadioController.SetFMCTCSS(On_: Boolean);
|
||
begin
|
||
FFMCTCSSOn := On_;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
|
||
Changed(rfFMCTCSS);
|
||
end;
|
||
|
||
procedure TRadioController.SetFMCTCSSTone(Idx: Integer);
|
||
begin
|
||
if Idx < 0 then Idx := 0 else if Idx > CTCSS_COUNT - 1 then Idx := CTCSS_COUNT - 1;
|
||
FFMCTCSSToneIdx := Idx;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
|
||
Changed(rfFMCTCSSTone);
|
||
end;
|
||
|
||
procedure TRadioController.SetFMSquelch(On_: Boolean);
|
||
begin
|
||
FFMSQOn := On_;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
|
||
Changed(rfFMSQ);
|
||
end;
|
||
|
||
procedure TRadioController.SetFMSquelchLevel(V: Integer);
|
||
begin
|
||
FFMSQLevel := V;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
|
||
Changed(rfFMSQLevel);
|
||
end;
|
||
|
||
procedure TRadioController.SetFMRpt(Dir: Integer);
|
||
begin
|
||
FFMRptDir := Dir;
|
||
FFMRptAutoActive := False; // явная установка направления — ручное действие
|
||
PushNetworkState; // TX-сдвиг применяется через ActiveTXFreqHz
|
||
Changed(rfFMRpt);
|
||
end;
|
||
|
||
procedure TRadioController.SetFMRptOffset(Hz: Double);
|
||
begin
|
||
if Hz <= 0.0 then Exit;
|
||
FFMRptOffsetHz := Hz;
|
||
if FFMRptDir <> RPT_NONE then PushNetworkState;
|
||
Changed(rfFMRpt);
|
||
end;
|
||
|
||
procedure TRadioController.UpdateRptAutoState;
|
||
// Авто-включение MINUS-сдвига в 2-метровом репитерном сегменте и авто-снятие
|
||
// при уходе из него (только если MINUS был выставлен автоматически).
|
||
begin
|
||
if RptAutoDir(FVfoA) = RPT_MINUS then
|
||
begin
|
||
if FFMRptDir = RPT_NONE then
|
||
begin
|
||
FFMRptDir := RPT_MINUS;
|
||
FFMRptAutoActive := True;
|
||
Changed(rfFMRpt);
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
if (FFMRptDir = RPT_MINUS) and FFMRptAutoActive then
|
||
begin
|
||
FFMRptDir := RPT_NONE;
|
||
FFMRptAutoActive := False;
|
||
Changed(rfFMRpt);
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.ApplyChannel(Idx: Integer);
|
||
begin Changed(rfChannel); { TODO wiring } end;
|
||
|
||
procedure TRadioController.StartStop;
|
||
begin SetRun(not FRunning); end;
|
||
|
||
procedure TRadioController.SetRun(On_: Boolean);
|
||
begin FRunning := On_; Changed(rfRunning); { TODO wiring: логика BtnStartStopClick } end;
|
||
|
||
procedure TRadioController.ConnectDevice(const Dev: THPSDRDevice);
|
||
begin { TODO wiring: DoConnectDevice } Changed(rfDevice); end;
|
||
|
||
procedure TRadioController.Discover;
|
||
begin { TODO wiring: FNetwork.Discover } end;
|
||
|
||
end.
|