mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
Всплесков на передаче нет, инструмент своё отработал и больше не нужен в горячем пути. Снято ровно то, что ставилd209697, и ничего сверх: * удалён юнит TxTrace.pas (кольцо + сброс дампа + TraceInit); * TCIAdapter: точки съёма маркеров и TX-аудио, поля FTxTraceLast/FTxTraceAudio; * HPSDRNetwork: DUC-DRY/DUC-RUN и переменная DryFrom (жила только для них), вместе с ними ушли из uses TxTrace и PlatformUtils — их добавлял тот же коммит, MonotonicUs в этом файле больше нигде не встречается; * RadioController: PTT-метка в SetMOX и сброс дампа на снятии PTT; * TraceInit из ewsdr.lpr и ewsdrd.lpr. Каждая удалённая строка стояла под `if TraceOn` — вычислений, от которых зависит тракт, в них не было. ★PlatformUtils.MonotonicUs НЕ тронут: на нём абсолютные дедлайны планировщика TX, это не отладка. Скрипт разбора test/hpsdr/txtrace_scan.py оставлен, формат дампа не менялся; в README стенда записан рецепт возврата зондов изd209697. Проверено: lazbuild --ws=qt6 и build-ewsdrd.sh собираются, test/tci 260/260, test/cat 50/50 — те же числа, что и с зондами. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
8276 lines
408 KiB
ObjectPascal
8276 lines
408 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, SyncObjs,
|
||
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
||
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder,
|
||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, CWMorse, CWKeyer,
|
||
CWDecoder;
|
||
|
||
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
||
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
|
||
// Допустимые rate пан-DDC — полная сетка железа Caps.RatePresets (кратные
|
||
// 48 кГц — требование движка), см. PanRateAllowed. Дефолт/фолбэк — 384 кГц.
|
||
|
||
type
|
||
// QO-100 beacon lock — статус контура (для UI/маркеров).
|
||
TBeaconState = (bsIdle, // лок выключен
|
||
bsSearch, // включён, маяк ещё не подтверждён (низкий prominence)
|
||
bsLock); // маяк уверенно виден и сопровождается
|
||
|
||
// Поле, которое изменилось — для гранулярных уведомлений наружу.
|
||
TRadioField = (
|
||
rfVfoA, rfVfoB, rfActiveVfo, rfVfoLock,
|
||
rfMode, rfFilter, rfFilterBW,
|
||
rfAGCMode, rfAGCTop,
|
||
rfRxGain, // Pluto: hw-gain (auto/manual dB)
|
||
rfVolume, rfMute, rfDrive, rfAtten,
|
||
rfBand, rfXvtr, rfBandRestore, // rfBandRestore: явная смена диапазона (сброс Wf-avg в UI)
|
||
rfCenterFreq, rfSpan, rfSampleRate, rfZoom,
|
||
rfCTun, rfDuplex, rfRxMuteOnTx, // rfRxMuteOnTx: QO-100 self-monitor toggle
|
||
rfRunning, rfConnected, rfTransmitting, rfTuning,
|
||
rfPureSignal, rfTwoTone, // PureSignal вкл/статус; 2TON вкл/выкл
|
||
rfNR, rfNB, rfSNB, rfANF,
|
||
rfWfAGC, rfWfNF, rfDither, rfRandom,
|
||
rfFMDeviation, rfFMCTCSS, rfFMCTCSSTone, rfFMSQ, rfFMSQLevel,
|
||
rfFMStep, rfFMStepIdx, rfFMRpt,
|
||
rfChannel,
|
||
rfTXProfile, // сменился активный TX-профиль
|
||
rfSMeter, rfPower, rfSWR, rfSupply, rfPLL, // телеметрия
|
||
rfADCOverload, // ADC overload (рендер индикатора)
|
||
rfBeaconLock, // QO-100 beacon lock: статус/коррекция
|
||
rfDevice, // подключённое устройство сменилось
|
||
rfDeviceList, // список discovered устройств обновился
|
||
rfPanFreq, // центр доп. пана уехал (ретюн DDC извне)
|
||
rfSliceFreq, // слайс перестроен извне (CAT); Id — FSliceFreqId
|
||
rfSliceState, // у слайса сменились мода/фильтр/АРУ/DSP/громкость; Id — FSliceFreqId
|
||
rfCWSettings, // правка телеграфа (скорость, задержка, pitch…)
|
||
rfMonVolume // громкость self-monitor'а (TX), отдельно от rfVolume
|
||
);
|
||
|
||
TRadioStateEvent = procedure(Sender: TObject; Field: TRadioField) of object;
|
||
|
||
// Маршалинг произвольного метода в поток контроллера (GUI: TThread.Synchronize).
|
||
TUIInvokeEvent = procedure(M: TThreadMethod) of object;
|
||
|
||
// Перехват RX-аудио потребителем (web-адаптер): вернуть True, если аудио
|
||
// отправлено клиенту и НЕ должно идти в локальный AudioOut. Вызывается из
|
||
// DSP-потока (как сам OnAudioReady) — реализация должна быть thread-safe.
|
||
TAudioConsumeEvent = function(const Left, Right: array of Single;
|
||
Count: Integer): Boolean of object;
|
||
|
||
// Тап RX-аудио: «послушать, не забирая», в отличие от OnAudioConsume. Нужен
|
||
// потокам TCI (§3.4): их может быть несколько, и ни один не имеет права
|
||
// отбирать звук у локальной звуковухи или у web-клиента.
|
||
// rakDemod — ДО громкости и мьюта, без сайдтона (аудиопоток приёмника);
|
||
// rakLineOut — то, что реально уходит на выход (поток линейного выхода).
|
||
// PanId: 0 = главный тракт, 1.. = доп. пан. SliceId — слайс-источник
|
||
// (0 у главного тракта): по нему потребитель отличает канал A от канала B.
|
||
// Вызывается из DSP-потока: обработчик обязан быть быстрым и не ждать.
|
||
TRadioAudioKind = (rakDemod, rakLineOut);
|
||
TRadioAudioTapEvent = procedure(Kind: TRadioAudioKind; PanId, SliceId: Integer;
|
||
const Left, Right: array of Single;
|
||
Count: Integer) of object;
|
||
|
||
// Сырьё спектра/водопада наружу (фронтенды рисуют). Вызывается из DSP-потока.
|
||
TPixelDataEvent = procedure(const Pixels: array of Single; Count: Integer) of object;
|
||
|
||
// Лента телеграфа для окна-терминала: прибавка текста и чья она — принятая
|
||
// декодером или эхо собственной передачи (окно красит их по-разному).
|
||
// Зовётся из потока хозяина (ServiceCWDecoder), не из DSP.
|
||
TCWLogEvent = procedure(const S: string; IsTX: Boolean) of object;
|
||
|
||
// Снимок состояния для синхронизации нового фронтенда (Arduino/Web на connect).
|
||
TRadioSnapshot = record
|
||
VfoA, VfoB, CenterFreq, SpanHz: Double;
|
||
SampleRate: Integer;
|
||
ActiveVfo: Integer;
|
||
Mode, Filter, FilterBW: Integer;
|
||
AGCMode, AGCTop: Integer;
|
||
RxGainMode: Integer;
|
||
RxGainDb: 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 = class;
|
||
|
||
// Event adapter: every DMR slice owns a decoder, while its asynchronous
|
||
// audio callback still has to retain the originating logical slice id.
|
||
TDMRSliceDecoder = class
|
||
private
|
||
FOwner: TRadioController;
|
||
FSliceId: Integer;
|
||
FDecoder: TDMRDecoder;
|
||
procedure AudioReady(const PCM: array of Single; Count: Integer);
|
||
public
|
||
constructor Create(AOwner: TRadioController; ASliceId: Integer);
|
||
destructor Destroy; override;
|
||
procedure FeedAudio(const Left, Right: array of Single; Count: Integer);
|
||
procedure GetStatus(out S: TDMRStatus);
|
||
end;
|
||
|
||
// Слайс уровня контроллера: логическая обёртка над WDSP-слайс-каналом
|
||
// (в TWDSPEngine) + собственный аудио-выход со своей звуковухой.
|
||
TCtrlSlice = record
|
||
Active: Boolean;
|
||
Id: Integer; // = id слайса в TWDSPEngine
|
||
Letter: Char; // буква флага ('A'..'F'), пока для UI
|
||
PanId: Integer; // пан слайса: 0 = главный, 1.. = доп. DDC-пан
|
||
TargetHz: Double; // абсолютная частота слайса
|
||
Mode: Integer;
|
||
FilterLo: Integer;
|
||
FilterHi: Integer;
|
||
AGC: TWDSPAGCMode;
|
||
Volume: Double; // 0..1
|
||
Muted: Boolean;
|
||
RxMuteOnTx: Boolean; // True (умолч.) = динамик слайса молчит на передаче
|
||
FMSQOn: Boolean; // FM-squelch включён (слайс в режиме FM)
|
||
FMSQLevel: Integer; // порог squelch 0..100
|
||
// Зеркало DSP-состояния слайса (движок его не отдаёт обратно): нужно UI
|
||
// флага и CAT-геттерам. Пишется только через SetSliceDSP.
|
||
NRMode: Integer; // 0=выкл, 1=NR, 2=NR2 (как у главного)
|
||
NBMode: Integer; // 0=выкл, 1=NB, 2=NB2
|
||
SNB: Boolean;
|
||
ANF: Boolean;
|
||
DeviceIndex: Integer; // PortAudio-индекс вывода (-1 = default)
|
||
DevName: string;
|
||
Audio: TAudioOutput; // владеет; свой поток вывода
|
||
// Вход слайса (mic/виртуальный кабель) — источник модуляции, когда именно
|
||
// этот слайс выбран TX-источником. -1/'' = брать общий вход (FAudioIn).
|
||
InDeviceIndex: Integer; // PortAudio-индекс ввода (-1 = общий)
|
||
InDevName: string;
|
||
AudioIn: TAudioInput; // НЕ владеет: ссылка на поток из mic-пула
|
||
// TX-профиль слайса ПО РЕЖИМУ: индекс в FTXProfiles или -1 = «не
|
||
// переключать». Применяется, когда слайс становится TX-источником
|
||
// (SetTxSlice) и когда он меняет режим, будучи источником.
|
||
TXProfByMode: TTXProfModeTable;
|
||
DMR: TDMRSliceDecoder; // владеет; nil для не-DMR слайса
|
||
end;
|
||
|
||
// POD-снимок слайса для фронтендов, которые читают модель из своих потоков
|
||
// (TCI-сервер: команда исполняется в потоке клиента). Только скаляры: копия
|
||
// TCtrlSlice тащит за собой managed-поля (DevName/InDevName) и объекты, а
|
||
// копирование строки из чужого потока, пока UI-поток её же переписывает,
|
||
// портит счётчик ссылок — это уже не рассинхрон, а порча кучи.
|
||
TSliceView = record
|
||
Id: Integer;
|
||
PanId: Integer;
|
||
TargetHz: Double;
|
||
Mode: Integer;
|
||
FilterLo: Integer;
|
||
FilterHi: Integer;
|
||
AGC: TWDSPAGCMode;
|
||
Volume: Double;
|
||
Muted: Boolean;
|
||
FMSQOn: Boolean;
|
||
FMSQLevel: Integer;
|
||
NRMode: Integer;
|
||
NBMode: Integer;
|
||
SNB: Boolean;
|
||
ANF: Boolean;
|
||
end;
|
||
|
||
{ TRadioController }
|
||
TRadioController = class
|
||
private
|
||
FOnInvoke: TUIInvokeEvent;
|
||
FOnStateChanged: TRadioStateEvent;
|
||
FStateListeners: array of TRadioStateEvent; // доп. наблюдатели (CAT/Andromeda), помимо FOnStateChanged (MainForm)
|
||
FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA
|
||
FOnBeforeStop: TThreadMethod; // хук перед гашением панов в StopRunning (персист панов)
|
||
FOnAudioConsume: TAudioConsumeEvent; // web-перехват RX-аудио (если активен клиент)
|
||
FLocalAudio: Boolean; // локальный звук (динамик/мик ПК): GUI=True, демон=False
|
||
FOnSpectrumData: TPixelDataEvent; // сырьё спектра наружу (UI рисует FSpecView)
|
||
FOnWaterfallData: TPixelDataEvent; // сырьё водопада наружу (UI: децимация+рендер)
|
||
// Тапы RX-аудио (TCI-потоки). Список короткий и меняется редко, но пишет
|
||
// его поток контроллера, а читает DSP — отсюда лок: снять тап на лету
|
||
// (клиент ушёл) иначе означало бы вызов метода освобождённого объекта.
|
||
FAudioTaps: array of TRadioAudioTapEvent;
|
||
FAudioTapLock: TCriticalSection;
|
||
FAudioTapCount: LongInt; // быстрый гейт: без тапов не берём и лок
|
||
FIQTap: TIQTapEvent; // тап IQ (TCI): помним ради пересоздания движка
|
||
procedure Changed(Field: TRadioField);
|
||
procedure FireAudioTap(Kind: TRadioAudioKind; PanId, SliceId: Integer;
|
||
const Left, Right: array of Single; Count: Integer);
|
||
// --- Мультислайсы ---
|
||
function FindSliceIndex(Id: Integer): Integer; // -1 если нет
|
||
procedure OnSliceAudioReady(SliceId: Integer;
|
||
const Left, Right: array of Single; Count: Integer);
|
||
procedure OnSliceDemodAudioReady(SliceId: Integer;
|
||
const Left, Right: array of Single; Count: Integer);
|
||
procedure OnSliceDMRAudioReady(SliceId: Integer;
|
||
const PCM: array of Single; Count: Integer);
|
||
function SliceMutedByTx(Idx: Integer): Boolean; // слайс молчит на передаче?
|
||
// Пул входных потоков: захват/освобождение ссылки на устройство ввода.
|
||
function AcquireMicInput(DevIndex: Integer): TAudioInput;
|
||
procedure ReleaseMicInput(var AI: TAudioInput);
|
||
procedure SetSliceInDeviceIdx(Idx, DevIndex: Integer; const DevName: string);
|
||
procedure FreeMicPool; // финальная зачистка (FreeEngines)
|
||
procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра
|
||
// --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) ---
|
||
procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст
|
||
procedure PanZoomStepTo(PanId: Integer; NewZ: Double); // зум с якорем на центре окна
|
||
function PanCenterHz(PanId: Integer): Double; // центр пана (0 = FCenterFreq)
|
||
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
|
||
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
|
||
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
|
||
// Единственная точка, где выбранный слот превращается в состояние тракта
|
||
// (FFilter/кромки/производная BW/FM-девиация). Тракт не трогает — только state.
|
||
procedure ApplyFilterSlot(Idx: Integer);
|
||
function ClampFilterIdx(Mode, Idx: Integer): Integer;
|
||
procedure CacheFilterToBand; // фильтр текущего банда в band-cache
|
||
function ActiveVfoHz: Double; // частота активного VFO (A или B)
|
||
function AnchoredPan(AZoom: Double): Double; // пан, центрирующий окно на маркере VFO
|
||
procedure ApplyTuneCore(VfoHz: Double); // CTUN shift/center/scroll для актив. VFO
|
||
public
|
||
// ============================================================
|
||
// ПУБЛИЧНЫЕ ПОЛЯ (миграционные — имена совпадают с TMainForm).
|
||
// Внешний чистый доступ для адаптеров — через команды / GetSnapshot /
|
||
// OnStateChanged. Прямая запись извне — временная мера на время Фазы 1.
|
||
// ============================================================
|
||
|
||
// ---- Последний кадр спектра/водопада (пиксели от WDSP) ----
|
||
// Снимок для исходящего web-зеркала (PushState читает напрямую). Пишется из
|
||
// DSP-потока в OnSpectrumReady/OnWaterfallReady; читается из GUI-таймера —
|
||
// та же толерантная гонка, что и раньше (косметика пикселей).
|
||
FSpectrumBuf: array[0..SPECTRUM_PIXELS-1] of Single;
|
||
FWaterfallBuf: array[0..SPECTRUM_PIXELS-1] of Single;
|
||
FSpectrumBufCount: Integer;
|
||
FWaterfallBufCount: Integer;
|
||
// Потолок точек кадра для web (настройка web.spec_pixels, 1024/2048/4096).
|
||
// Кадр анализатора шире — децимируем до него; уже — уходит как есть.
|
||
FWebSpecPixels: Integer;
|
||
// Водопад копится max-hold'ом до тех пор, пока PushState не заберёт строку
|
||
// (анализатор даёт до 60 кадров/с, web шлёт 20 строк/с). Сбрасывает флаг
|
||
// тот, кто забрал строку, — тогда следующий кадр начинает новую.
|
||
FWfAccumFresh: Boolean;
|
||
|
||
// ---- Движки (владение: см. шапку юнита) ----
|
||
// FNetwork — абстрактный бэкенд железа (TRadioBackend): конкретный класс
|
||
// (THPSDRNetwork / TPlutoBackend позже) выбирается при подключении.
|
||
FNetwork: TRadioBackend;
|
||
FDSPEngine: TWDSPEngine;
|
||
FAudioOut: TAudioOutput;
|
||
FAudioIn: TAudioInput;
|
||
|
||
// ---- Мультислайсы (софт-fan-out) ----
|
||
// Слайс = доп. приёмник (свой WDSP-канал в движке) + своя звуковуха.
|
||
// TargetHz абсолютна; ShiftHz в движок = TargetHz - FCenterFreq.
|
||
FSlices: array[0..MAX_SLICES-1] of TCtrlSlice;
|
||
FNextSliceId: Integer; // монотонный генератор id слайсов
|
||
|
||
// ---- Пул входных потоков (mic) ----
|
||
// Каждому УНИКАЛЬНОМУ устройству ввода — один открытый TAudioInput на всё
|
||
// приложение; слайсы держат на него ссылку по refcount. Потоки живут, пока
|
||
// на них кто-то ссылается: переключение TX-источника = смена указателя,
|
||
// без Close/Open (иначе на старте передачи терялись бы первые сэмплы).
|
||
// Читатель у кольца всегда один — TX-источник в данный момент только один.
|
||
FMicPool: array of record
|
||
DevIdx: Integer;
|
||
Refs: Integer;
|
||
AI: TAudioInput;
|
||
end;
|
||
FSettings: TSettingsManager;
|
||
FChannelStore: TChannelStore;
|
||
FDeviceStore: TDeviceStore; // общий список устройств (saved+discovered)
|
||
FWDSPReady: Boolean;
|
||
|
||
// ---- Частоты / режим / фильтр / AGC ----
|
||
FVfoA, FVfoB: Double;
|
||
FActiveVfo: Integer;
|
||
FMode: Integer;
|
||
FFilter: Integer; // слот 0..FILT_SLOTS-1 (10/11 = VAR1/VAR2)
|
||
// Знаковые кромки текущего фильтра — ИСТИНА. FFilterBW производная
|
||
// (FilterBWFromEdges) и оставлена ради прежних читателей (web, зум,
|
||
// band-cache, CAT); писать в неё напрямую нельзя.
|
||
FFilterLo: Integer;
|
||
FFilterHi: Integer;
|
||
FFilterBW: Integer;
|
||
FFilterTable: TFilterTable; // редактируемые фильтры по режиму
|
||
FAGCMode: Integer;
|
||
FAGCTop: Integer;
|
||
FRxGainMode: Integer; // Pluto hw-gain: 0=manual,1=fast,2=slow,3=hybrid
|
||
FRxGainDb: Integer; // Pluto manual gain (дБ, 0..73)
|
||
FCTun: Boolean;
|
||
FCenterFreq: Double;
|
||
FSpanHz: Double;
|
||
FSampleRate: Integer;
|
||
FZoomFactor: Double; // зум спектра/водопада 0.05..1.0 (1.0 = без зума)
|
||
FPanSlider: Double; // положение окна зума 0..1 (0.5 = центр)
|
||
FCurrentBand: Integer;
|
||
FCurrentXvtr: Integer;
|
||
|
||
// ---- QO-100 beacon lock (декодер маяка 10489.750 = эталон частоты) ----
|
||
// Один концепт: лок = декодер + трим LOError. Декодер точно ведёт несущую,
|
||
// его измеренный дрейф стекаем в LOError → весь даунлинк стабилен.
|
||
FBeaconLockOn: Boolean; // лок (=декодер) включён пользователем
|
||
FBeaconLockHz: Double; // коррекция, накопленная за ТЕКУЩУЮ сессию лока
|
||
// (только индикация на кнопке); сама поправка
|
||
// живёт в LOError активного трансвертера
|
||
FBeaconRefHz: Double; // опорная частота маяка (Hz, downlink), деф. 10489.750e6
|
||
FBeaconTrackHz: Double; // измеренная АБСОЛЮТНАЯ display-частота маяка
|
||
// (наведение + остаток декодера) — для оранж. маркера
|
||
FBeaconSNRdB: Double; // SNR из последнего снимка декодера (индикация лока)
|
||
FBeaconBal: Double; // EMA доли меньшинства знаков I (BPSK-ность)
|
||
FBeaconLocked: Boolean; // декодер солидно залочен (гистерезис по SNR)
|
||
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка ретюна LO)
|
||
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
|
||
// ---- декодер маяка (BPSK 400 бод, AO-40) — фронтенд+констелляция+бюллетень ----
|
||
FBeaconDec: TBeaconDecoder; // владеет; тап IQ в WDSPEngine
|
||
FBeaconDecHz: Double; // АБСОЛЮТНАЯ display-частота наведения декодера
|
||
// (узкий фильтр, ставится кликом); 0 = ещё не наведён
|
||
FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True
|
||
// Лупа наведения (окно Beacon) — состояние окна, переживающее перезапуск.
|
||
// Центр хранится СДВИГОМ от FBeaconRefHz: так он не зависит ни от правки
|
||
// опорной частоты, ни от накопленной калибровки LOError.
|
||
FBcnZoomSpanHz: Double; // ширина окна лупы, Гц
|
||
FBcnZoomOffsetHz: Double; // центр = FBeaconRefHz + этот сдвиг
|
||
FBcnZoomFollow: Boolean; // держать наведение декодера в окне
|
||
FBcnZoomAuto: Boolean; // автопоиск маяка в окне лупы
|
||
|
||
// ---- DMR 4FSK receive decoder ----
|
||
FDMRDec: TDMRDecoder; // 48 kHz FM discriminator -> DMR worker
|
||
|
||
// ---- Аудио / TX ----
|
||
FVolume: Integer; // громкость RX
|
||
FTxMonVolume: Integer; // громкость self-monitor RX-аудио на TX (RX MUTE off, DUP)
|
||
FMuted: Boolean;
|
||
FDrivePercent: Integer; // драйв 0..100 % (UI-уровень, источник истины)
|
||
FDriveLevel: Byte; // вычисляемый hardware-байт (CalcDriveByte)
|
||
FLastVfoScrolled: Boolean; // последний SetVfoA прокрутил центр (для водопада)
|
||
FAtten: Integer;
|
||
FPAMaxPower: Double;
|
||
FPlutoTxMaxAttDb: Double; // Pluto: TX-аттенюация на drive=100% (потолок, дБ<0)
|
||
FRunning: Boolean;
|
||
FTransmitting: Boolean;
|
||
FTuning: Boolean;
|
||
// ---- PureSignal ----
|
||
FPSEnabled: Boolean; // авто-калибровка PS включена (кнопка PS)
|
||
FPSSingleCal: Boolean; // запрошена одиночная калибровка
|
||
FPSResetReq: Boolean; // запрошен сброс коррекции (OFF)
|
||
FPSCmdState: Integer; // eCMDState Thetis (см. PureSignalTick)
|
||
FPSAAState: Integer; // auto-attenuate state (0=monitor,1=set,2=restore)
|
||
FPSAASaveAuto: Integer; // сохранённый automode на время смены атт.
|
||
FPSAASaveSingle: Integer;
|
||
FPSAADeltaDB: Integer;
|
||
FPSInfo: array[0..15] of Integer; // кэш GetPSInfo (UI-поток)
|
||
FPSInfoPrev: array[0..15] of Integer;
|
||
FPSMaxTX: Double; // кэш GetPSMaxTX
|
||
// Feedback-тракт вооружён (сеть DDC0/1 + pscc в движке). Пишет UI-поток
|
||
// (PureSignalTick/PSSetFeedbackPath), читает сетевой (OnDDCIQ).
|
||
FPSFeedbackOn: Boolean;
|
||
FTwoTone: Boolean; // 2TON активен
|
||
FSplitTxB: Boolean;
|
||
FTxSliceId: Integer; // 0 = главный VFO (A/B split); >0 = слайс-источник TX
|
||
// Id слайса, перестроенного извне (CAT): полезная нагрузка rfSliceFreq —
|
||
// Changed несёт только поле, а обработчику нужен конкретный флаг.
|
||
FSliceFreqId: Integer;
|
||
// Auto TX слайса (индекс = СЛОТ слайса, не Id): PTT по CAT-порту слайса сам
|
||
// назначает его TX-источником. Флаг живёт на слоте, а не на слайсе, потому
|
||
// что настройка привязана к букве (B..G) и переживает удаление слайса.
|
||
FSliceAutoTx: array[0..MAX_SLICES-1] of Boolean;
|
||
// Аудио-устройства слота (буквы B..G): '' = общий выход приложения / общий
|
||
// вход. Это НЕ шаблон, а привязка слота: применяется к живому слайсу сразу
|
||
// и достаётся новому слайсу, который встанет на этот слот.
|
||
FSliceOutDev: array[0..MAX_SLICES-1] of string;
|
||
FSliceInDev: array[0..MAX_SLICES-1] of string;
|
||
FVfoLock: Boolean; // блокировка перестройки VFO (Andromeda Lock)
|
||
FDisplayDuplex: Boolean;
|
||
// QO-100 full-duplex self-monitor: True — RX-аудио глушится на TX (по умолч.),
|
||
// False — слышим свой downlink со спутника при передаче. Кнопка RX MUTE.
|
||
FRxMuteOnTx: Boolean;
|
||
FHWPTTActive: Boolean;
|
||
FHWPTTStartedTX: Boolean;
|
||
FWebClientActive: Boolean; // выставляет web-слой (connect/disconnect клиента)
|
||
FWebMicActive: Boolean; // True если текущий TX идёт через txmsWeb
|
||
FTCIMicRequested: Boolean; // TCI-клиент попросил TRX:…,tci
|
||
FTCIMicActive: Boolean; // True если текущий TX модулируется из TCI
|
||
FTCITxLeadFrames: Integer; // аванс подачи TCI, отсчётов 48 кГц (ставит адаптер)
|
||
FSendAudioToRadio: Boolean;
|
||
FAudioOutDevName: string;
|
||
FAudioInDevName: string;
|
||
FDevConnected: Boolean;
|
||
FDiscoverNoneFound: Boolean; // discovery завершился без устройств (для рендера)
|
||
|
||
// ---- DSP-фильтры шума / водопад ----
|
||
FNRMode, FNBMode: Integer; // источник истины (раньше Btn*.Tag)
|
||
FSNB, FANF: Boolean; // источник истины (раньше Btn*.Tag)
|
||
FWfAGCEnabled: Boolean;
|
||
FWfNFEnabled: Boolean;
|
||
FDitherEnabled: Boolean;
|
||
FRandomEnabled: Boolean;
|
||
|
||
// ---- Display state (persist в TGlobalSettings; рендерится FSpecView/Wideband) ----
|
||
FShowSpectrum: Boolean;
|
||
FShowWaterfall: Boolean;
|
||
FShowWideband: Boolean;
|
||
FWidebandFill: Boolean;
|
||
FSpectrumFill: Boolean;
|
||
FWfManualHigh: Double;
|
||
FWfManualLow: Double;
|
||
FWfAGCOffset: Double;
|
||
FWfPalette: Integer; // 0=Classic, 1=Inferno, 2=Turbo
|
||
FWfGamma: Double; // контраст палитры водопада
|
||
FDisplayPixels: Integer; // потолок точек анализатора
|
||
FWfSpeedDiv: Integer; // делитель скорости водопада (1/2/4/8, max-hold)
|
||
FSpecMSAA: Integer; // MSAA GL-спектра (1=off, 2, 4, 8)
|
||
FSpecRefLevel: Double;
|
||
FSpecRange: Double;
|
||
FSpecGridStep: Double;
|
||
FTXSpecRefLevel: Double; // TX-сетка (Thetis-style): применяется при FTransmitting
|
||
FTXSpecRange: Double;
|
||
FTXSpecGridStep: Double;
|
||
|
||
// ---- FM ----
|
||
FFMDeviation: Double;
|
||
FRawFMDeviation: 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;
|
||
// Именованные TX-профили («звуковая» часть FTXSettings + drive, см.
|
||
// TTXProfile). Истина для эфира — всегда живые FTXSettings/FDrivePercent;
|
||
// активный профиль ведётся за ними (StoreActiveTXProfile).
|
||
FTXProfiles: TTXProfileList;
|
||
// Телеграф: параметры кейера/сайдтона/pitch (DUC Specific 5..13,17 + CW-тракт).
|
||
FCWSettings: TCWSettings;
|
||
// Программная передача текста (бит CWX). Поток создаётся лениво — у тех,
|
||
// кто телеграфом не пользуется, лишней нити не заводится.
|
||
FCWSender: TCWSender;
|
||
// Чужая манипуляция по сети (TCI KEYER): очередь готовых интервалов.
|
||
// Тоже лениво — заводится на первом элементе.
|
||
FCWElem: TCWElemPlayer;
|
||
// Состояние ключа для ПРОГРАММНОГО сайдтона. Пишется потоком передачи и
|
||
// разбором HP-статуса (прямой ключ), читается DSP-потоком в MixSidetone —
|
||
// толерантная гонка на одном булеве, как FHPp*.
|
||
FCWKeyDown: Boolean;
|
||
// Прошлое состояние лепестков — обрыв программной передачи по фронту.
|
||
FCWPrevDot: Boolean;
|
||
FCWPrevDash: Boolean;
|
||
// Момент последнего замыкания ключа прошивкой — только для ИНДИКАЦИИ
|
||
// (см. RadioKeyed): без выдержки метр моргал бы RX/TX на каждой точке.
|
||
FCWKeyedAt: QWord;
|
||
// Предыдущее значение RadioKeyed — событие рендера шлём только на фронте.
|
||
FCWKeyedShown: Boolean;
|
||
// Последний отправленный байт 5 — чтобы не гнать DUC-пакет на каждую
|
||
// смену режима, а только когда телеграфная конфигурация реально поменялась.
|
||
FLastCWOptions: Byte;
|
||
FCWSTPhase: Double; // фаза генератора (только DSP-поток)
|
||
FCWSTEnv: Double; // огибающая 0..1, сглаживает щелчки (DSP-поток)
|
||
FCWSTBufL: array of Single; // буферы подмешивания (только DSP-поток)
|
||
FCWSTBufR: array of Single;
|
||
FCWSTEnvBuf: array of Single; // огибающая от локального кейера (DSP-поток)
|
||
// ---- Локальный кейер (CWKeyer.pas) --------------------------------------
|
||
// Генератор несущей с огибающей прямо в поток IQ. Обязателен там, где кейера
|
||
// в железе нет (Pluto/LibreSDR), и опционален у openHPSDR. Оба объекта
|
||
// создаются лениво — кто телеграфом не пользуется, лишних нитей не получает.
|
||
FCWLocal: TCWLocalKeyer;
|
||
FCWKeyPort: TCWKeyPort;
|
||
// Сессия локального кейера подняла передачу сама (break-in): по этому флагу
|
||
// её снимаем и не путаем с ручным MOX.
|
||
FCWLocalTX: Boolean;
|
||
// Локальный кейер вооружён — считается в SyncCWKeyer, читается пейсингом
|
||
// частоты DUC (CWCarrierOffsetHz) и аттенюатором Pluto.
|
||
FCWLocalArmed: Boolean;
|
||
// ---- Декодер приёма (CWDecoder.pas) --------------------------------------
|
||
// Создаётся лениво вместе с окном-терминалом. Кормится из OnDemodAudioReady
|
||
// (до громкости и мьюта, без сайдтона), разбирается на таймере хозяина.
|
||
FCWDec: TCWDecoder;
|
||
// Лента расшифровки: последние знаки приёма (для полосы под спектром) и
|
||
// счётчик, по которому потребители понимают, что появилось новое.
|
||
FCWLogText: string;
|
||
FCWLogSeq: Int64;
|
||
FOnCWLog: TCWLogEvent;
|
||
FAlexSettings: TAlexSettings;
|
||
FOCSettings: TOCSettings; // OC Control (Open Collector, openHPSDR-only)
|
||
// Антенные разъёмы AD936x (Pluto/LibreSDR) на диапазон/трансвертер.
|
||
FAnt936x: TAD936xAnt;
|
||
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;
|
||
// Калибровки (детектор мощности/КСВ, напряжение/ток, S-метр) — вкладка
|
||
// Calibration. Читается сетевым потоком в OnHPStatus (double-поля, torn
|
||
// read безопасен как у FPABandCal); менять через SetCalibration.
|
||
FCal: TCalibration;
|
||
FDevMAC: array[0..5] of Byte;
|
||
// Глобальные настройки устройства, загруженные ConnectDevice. Хранятся для
|
||
// UI-рендера полей, которых нет в контроллере (DisplayFPS/FreqMhzDigits/CAT).
|
||
FLoadedGlobal: TGlobalSettings;
|
||
|
||
// ---- Телеметрия (наполняется в Фазе 3/4 вместе с сетевыми колбэками) ----
|
||
FLastSMeter, FLastFwdW, FLastSWR, FLastSupplyV, FLastSupplyA: Double;
|
||
FLastPLLLock: Boolean;
|
||
// Pluto-телеметрия (нет HP-status): температура трансивера °C / Zynq SoC °C /
|
||
// RSSI приёмника дБ. Опрашиваются ServicePlutoTelemetry с UI-таймера;
|
||
// TELEMETRY_NONE = нет данных.
|
||
FLastPlutoTempC, FLastPlutoSocTempC, FLastPlutoRSSIdB: Double;
|
||
// Фактическое усиление RX-тракта AD936x (дБ) — в режимах AGC его крутит
|
||
// железо. Снимается тем же опросом; UI показывает его в блоке RF AGC.
|
||
FLastPlutoGainDb: Double;
|
||
FPlutoTeleTick: QWord; // GetTickCount64 последнего опроса (троттлинг)
|
||
FPlutoTempBad: Integer; // подряд отброшенных выбросов температуры (анти-залипание)
|
||
|
||
// ---- RX/seq телеметрия data-path (пишется из сетевого колбэка OnDDCIQ) ----
|
||
FRXPacketCount: LongWord; // счётчик принятых IQ пакетов
|
||
FRXStartTime: QWord; // GetTickCount64 момента START
|
||
FRXLastPktTime: QWord; // GetTickCount64 последнего принятого пакета
|
||
FActiveDDC: Integer; // DDC index для текущей платы (0 или 2)
|
||
// ---- Панадаптеры на аппаратных DDC (этап 3.1) ----
|
||
// PanId 1..MAX_PANS-1 → hw DDC index (-1 = пан не создан); пан 0 = главный
|
||
// тракт (FActiveDDC). FDDCPanMap — обратный маппинг DDC→PanId для роутинга
|
||
// IQ в OnDDCIQ (пишется под FSliceLock-дисциплиной: только UI-поток).
|
||
FPanDDCIdx: array[1..MAX_PANS-1] of Integer;
|
||
FPanFreqHz: array[1..MAX_PANS-1] of Double;
|
||
FPanRateKHz: array[1..MAX_PANS-1] of Word;
|
||
FPanADC: array[1..MAX_PANS-1] of Byte; // ADC-источник пана (3.4)
|
||
FPanZoomF: array[1..MAX_PANS-1] of Double; // зум пана 0..1 (0 = весь span)
|
||
FPanPanSl: array[1..MAX_PANS-1] of Double; // положение окна пана 0..1
|
||
FMainADCSrc: Byte; // ADC главного DDC (пан 0)
|
||
FDDCPanMap: array[0..MAX_DDCS-1] of Integer;
|
||
FLastDDCSeq: LongWord; // последний seq (из сетевого потока)
|
||
FLastDDCIndex: Integer; // последний DDC index
|
||
FDDCLastSeq: array[0..6] of LongWord;
|
||
FDDCSeqValid: array[0..6] of Boolean;
|
||
FSeqErrorCount: LongWord;
|
||
FLastSeqErrorDDC: Integer;
|
||
FLastSeqErrorDelta: Int64;
|
||
FSeqOkStreak: Integer;
|
||
FADCOverloadActive: Boolean; // ADC overload (рендерится по rfADCOverload)
|
||
// ---- TX DUC-очередь (накопление IQ от WDSP в пакеты по 240, OnTXIQ) ----
|
||
FDUCPendingI: array[0..239] of Integer;
|
||
FDUCPendingQ: array[0..239] of Integer;
|
||
FDUCPendingCount: Integer;
|
||
// Pending-значения декода HP-статуса (сетевой поток → ApplyHPStatus через
|
||
// Invoke; Synchronize блокирующий и последовательный, гонок нет).
|
||
FHPpFwdW, FHPpSWR, FHPpSupplyV, FHPpSupplyA: Double;
|
||
FHPpPLL, FHPpHWPTT: Boolean;
|
||
FHPpOverload: Byte;
|
||
// Pending discovered-устройство (сетевой поток discovery → ApplyDeviceFound
|
||
// через Invoke; Synchronize блокирующий — устройства приходят по одному).
|
||
FPendingFoundDev: THPSDRDevice;
|
||
FPendingFoundEntry: string;
|
||
// URI для probe сетевых Pluto при discovery (см. SetPlutoProbeURIs).
|
||
FPlutoProbeURIs: array of string;
|
||
|
||
constructor Create;
|
||
destructor Destroy; override;
|
||
|
||
// Lifecycle движков: создаёт FNetwork/FDSPEngine/FAudioOut/FAudioIn с
|
||
// заданным sample rate (вызывающий уже определил его из настроек). Колбэки
|
||
// движков навешивает вызывающий (GUI: методы MainForm; демон: свои). Парный
|
||
// FreeEngines закрывает и освобождает их; вызывается из Destroy.
|
||
procedure CreateEngines(ASampleRate: Integer);
|
||
procedure FreeEngines;
|
||
// Внутренние колбэки движка (аудио слайсов, отвод демодулятора, калибровка
|
||
// дисплея, тапы TCI). Зовёт CreateEngines — и ОБЯЗАН звать всякий, кто
|
||
// пересоздал FDSPEngine: без этого новый движок остаётся без отвода
|
||
// демодулятора (декодеры CW/DMR/FM RAW и RX_AUDIO по TCI) и без тапа IQ.
|
||
procedure AttachEngineCallbacks;
|
||
|
||
// ---- Колбэки движков (вызываются из рабочих потоков сети/DSP) ----
|
||
// Чистая обработка данных без UI; навешиваются на FNetwork/FDSPEngine
|
||
// вызывающим (GUI или демон) после CreateEngines.
|
||
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
|
||
procedure OnDemodAudioReady(const Left, Right: array of Single; Count: Integer);
|
||
procedure OnDMRAudioReady(const PCM: array of Single; Count: Integer);
|
||
// Общая выборка калибровочной таблицы «диапазон/трансвертер» по частоте.
|
||
function CalLookup(VisibleHz: Double;
|
||
const BandTbl: array of Double;
|
||
const XvtrTbl: array of Double): Double;
|
||
procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
|
||
procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
|
||
procedure OnMicPacket(const Data: TMicDataPacket);
|
||
function PullSoundCardMic(MaxN: Integer): Integer;
|
||
procedure OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket);
|
||
procedure OnHPStatus(const Status: THighPriorityStatus);
|
||
procedure ApplyHPStatus; // маршалится из OnHPStatus (сглаживание/overload/HWPTT)
|
||
procedure OnTXIQ(const Buf: array of Double; Count: Integer);
|
||
procedure OnDeviceFound(const Dev: THPSDRDevice); // сетевой поток discovery
|
||
procedure ApplyDeviceFound; // маршалится из OnDeviceFound (store + rfDeviceList)
|
||
procedure DiscoverFinishedNone; // маршалится из discover-потока: устройств нет
|
||
|
||
// Маршалинг команды в поток контроллера (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;
|
||
// XvtrTranslateTX — то же для ПЕРЕДАЧИ. Обычный трансвертер: = XvtrTranslate.
|
||
// QO-100 (FullDuplexTx): отдельный TX-LO (visible-TxLOOffset, без LOError —
|
||
// uplink-LO не подвержен дрейфу LNB).
|
||
function XvtrTranslateTX(VisibleHz: Double): Double;
|
||
function ActiveTXFreqHz: Double;
|
||
// TXTuneFreqHz — ЕДИНАЯ дверь «какую частоту передачи класть в железо»:
|
||
// ActiveTXFreqHz через трансвертер, минус сдвиг несущей локального кейера
|
||
// (CWCarrierOffsetHz). Прямых XvtrTranslateTX(ActiveTXFreqHz) в пушах
|
||
// состояния быть не должно — иначе телеграф уедет от VFO ровно на сдвиг.
|
||
function TXTuneFreqHz: Double;
|
||
// Режим передающего тракта: слайс-источник TX (FTxSliceId) либо главный FMode.
|
||
function ActiveTXMode: Integer;
|
||
// Диапазон для калибровки драйва/антенны TX (банд TX-слайса при кросс-банде).
|
||
function TxBandIdx: Integer;
|
||
// Выбор слайса-источника передачи (0 = главный VFO). Эксклюзивно.
|
||
procedure SetTxSlice(SliceId: Integer);
|
||
property TxSliceId: Integer read FTxSliceId;
|
||
// Складывает коррекцию маяка dLock в LOError активного трансвертера (живая
|
||
// калибровка LNB), ретюнит радио и троттлингом сохраняет на диск.
|
||
procedure ApplyBeaconLOError(dLock: Double);
|
||
// Flush LOError на диск при выключении лока (если троттлинг не успел).
|
||
procedure PersistBeaconCalibration;
|
||
|
||
// Применяет текущие FMode/FFilterBW/FM-параметры к DSP (RX/TX фильтр,
|
||
// FM-девиация, squelch). Вызывается командами и UI-путями смены режима.
|
||
procedure ApplyModeFilter;
|
||
|
||
// Вычисляет hardware drive-байт (Thetis-формула) из текущего состояния
|
||
// (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием.
|
||
function CalcDriveByte: Byte;
|
||
|
||
// Источник микрофона по умолчанию для MOX-кнопки/Web-TX: звуковая карта
|
||
// если настроена, иначе HW mic-стрим трансивера. HW PTT → txmsRadio в SetMOX.
|
||
function DefaultMicSource: TTXMicSource;
|
||
|
||
// ---- TX/DSP-цепь (чистые над состоянием: читают F*/FTXSettings, пишут в движок) ----
|
||
function BuildMicLineSelectByte: Byte; // DUC Specific byte 50 (mic/line биты)
|
||
procedure ApplyNoiseFilterButtonsToDSP; // NR/NB/SNB/ANF → WDSP
|
||
procedure SendDUCSpecificFromSettings; // DUC Specific (mic/keyer/atten) в сеть
|
||
procedure ApplyTXSettingsToDSP; // TX-цепь (фильтр/gain/EQ/comp/...) → WDSP
|
||
// ---- Телеграф ----------------------------------------------------------
|
||
// Живой контекст «где мы сейчас»: в трансвертере правки запоминаются в его
|
||
// слоте, иначе в кэше диапазона. ★Развилка обязана стоять у КАЖДОГО поля,
|
||
// которое помнится по диапазону: пока её не было у режима/фильтра/AGC/CTUN,
|
||
// трансвертер травил ими тот HF-диапазон, на котором стоял до входа
|
||
// (FCurrentBand при входе в слот не меняется).
|
||
function XvtrSlotActive: Boolean;
|
||
function TXProhibited: Boolean; // DoNotTx-бэнд / RX-only XVTR
|
||
function CWTXActive: Boolean; // манипуляцией владеет не MOX
|
||
function CWLocalSource: Boolean; // манипуляцию рисует PC
|
||
function CWFWSource: Boolean; // манипуляцию рисует прошивка
|
||
function CWOptionsByte: Byte; // DUC Specific байт 5
|
||
procedure SyncCWKeyer; // байт 5 под текущий TX-режим
|
||
procedure CWXKeyEvent(Down: Boolean); // колбэк потока передачи
|
||
procedure CWElemKey(Down: Boolean); // колбэк проигрывателя KEYER
|
||
// Сдвиг несущей манипуляции от гетеродина (уход от утечки LO у zero-IF).
|
||
// На него же уезжает DUC/LO, чтобы РЧ встала ровно на VFO — см. TXTuneFreqHz.
|
||
function CWCarrierOffsetHz: Double;
|
||
// ---- Локальный кейер ----------------------------------------------------
|
||
procedure EnsureCWLocal; // ленивое создание генератора
|
||
procedure PushCWLocalConfig; // настройки → генератор/порт
|
||
procedure CWLocalIQ(const Buf: array of Double; Count: Integer);
|
||
procedure CWLocalSession(Active: Boolean); // T/R под сессию манипуляции
|
||
procedure CWLocalTick; // «ключ живой» (индикация)
|
||
procedure CWKeyPortEvent(Dot, Dash: Boolean); // лепестки с COM-порта
|
||
// Подмешать программный сайдтон в блок RX-аудио (DSP-поток). Возвращает
|
||
// False, если подмешивать нечего — тогда вызывающий отдаёт исходный блок.
|
||
function MixSidetone(const Left, Right: array of Single;
|
||
Count: Integer): Boolean;
|
||
function PSOutlierSigmaEff: Double; // σ outlier-фильтра calcc (авто по плате)
|
||
|
||
// ---- TX-профили --------------------------------------------------------
|
||
// Профиль — именованный снимок «звуковой» части TX-цепи (состав в
|
||
// TTXProfile). Отдельной кнопки «сохранить» нет: правки ручками пишутся в
|
||
// активный профиль сразу (StoreActiveTXProfile), как и всё остальное в
|
||
// EWSDR. Переключение применяет снимок в эфир немедленно.
|
||
function TXProfileCount: Integer;
|
||
function TXProfileName(Idx: Integer): string;
|
||
function ActiveTXProfile: Integer;
|
||
// Единая дверь для правки TX-настроек снаружи (вкладка Transmit, CAT, web):
|
||
// применяет запись к движку/железу, дописывает активный профиль и персистит.
|
||
// Notify=False — вызывающий сам отрисует изменения (так делает MainForm,
|
||
// иначе rfTXProfile перезагрузил бы форму настроек прямо под руками).
|
||
procedure SetTXSettings(const T: TTXSettings; Notify: Boolean = True);
|
||
// Auto=True — переключение сделала автоматика привязок; ручной выбор
|
||
// (дропдаун, список настроек) дополнительно ЗАПОМИНАЕТСЯ за текущей парой
|
||
// «источник + модуляция» (BindTXProfileToCurrentMode).
|
||
procedure SelectTXProfile(Idx: Integer; Auto: Boolean = False);
|
||
procedure StoreActiveTXProfile(Persist: Boolean = False);
|
||
function AddTXProfile(const AName: string): Integer; // форк текущего звука
|
||
procedure RenameTXProfile(Idx: Integer; const AName: string);
|
||
procedure DeleteTXProfile(Idx: Integer);
|
||
procedure ResetTXProfilesToFactory; // заводской набор заново
|
||
// Профиль на TX-источник, ключ — режим этого флага (-1 = не переключать,
|
||
// звучит активный). Таблица у каждого флага своя: главный в FTXProfiles,
|
||
// слайсы в своих записях.
|
||
function FlagMode(SliceId: Integer): Integer;
|
||
procedure BindTXProfileToCurrentMode(Idx: Integer); // ручной выбор → в таблицу
|
||
function SliceTXProfile(SliceId: Integer): Integer;
|
||
procedure SetSliceTXProfile(SliceId, ProfIdx: Integer);
|
||
function SliceTXProfileTable(SliceId: Integer): TTXProfModeTable;
|
||
procedure SetSliceTXProfileTable(SliceId: Integer; const T: TTXProfModeTable);
|
||
procedure RemapTXProfileBindings(Deleted: Integer); // после удаления/замены списка
|
||
procedure ApplyBoundTXProfile; // профиль текущего TX-источника → в эфир
|
||
procedure SaveTXProfilesToConfig;
|
||
|
||
// ---- Управление диапазонами (состояние + DSP; рендер через события) ----
|
||
// MakeBandSettings — снимок текущего состояния в запись диапазона.
|
||
// SaveCurrentBand — сохраняет текущее состояние в кэш+конфиг (или в XVTR-слот).
|
||
// RestoreBand — загружает состояние диапазона, применяет к DSP, шлёт события
|
||
// рендера. Финальную перестройку VFO A делает вызывающий (UI: ApplyVfoA).
|
||
function MakeBandSettings: TBandSettings;
|
||
procedure SaveCurrentBand;
|
||
procedure RestoreBand(BandIdx: Integer);
|
||
|
||
// Глобальные настройки устройства (persist по MAC). BuildGlobalSettings
|
||
// собирает радио-поля из своего состояния, а НЕ-радио поля (тема/FPS/digits/
|
||
// CAT) переносит сквозь из FLoadedGlobal — контроллер их не трактует, только
|
||
// round-trip в файл. SaveGlobalSettings пишет блоб (гейт по FDevConnected).
|
||
function BuildGlobalSettings: TGlobalSettings;
|
||
procedure SaveGlobalSettings;
|
||
// Применяет DSP-настройки диапазона (mode/filter/AGC/CTUN/FM) + шлёт события
|
||
// рендера. НЕ трогает частоты VFO/центр — это переиспользуется как RestoreBand
|
||
// (с загрузкой VFO), так и SetActiveVfo (bandstack: смена бэнда без сброса VFO).
|
||
procedure ApplyBandDSP(const B: TBandSettings);
|
||
// Полоса фильтра для (режим, индекс фильтра) — выбор per-mode таблицы.
|
||
function FilterBWFor(M, Idx: Integer): Integer;
|
||
// Обратное отображение: ближайший индекс пресета к полосе BW (для CAT/UI,
|
||
// где полоса задана в Гц, а протокол оперирует индексом).
|
||
function FilterIdxFor(M, BW: Integer): Integer;
|
||
|
||
// ---- XVTR (трансвертер) ----
|
||
// ApplyXvtrToNetwork — XVTR enable bit + DisablePA + RX ant в сеть.
|
||
// ActivateXvtr — вход в слот: state+DSP (через ApplyBandDSP) + VFO/центр + сеть;
|
||
// рендер через rfXvtr/rfBand(XVTR-aware)/rfActiveVfo. UI: wideband+web.
|
||
// DeactivateXvtr — сохраняет слот (SaveCurrentBand) и выходит; HF-возврат
|
||
// (RestoreBand + финальная перестройка VFO) делает UI-обёртка.
|
||
procedure ApplyXvtrToNetwork;
|
||
procedure ActivateXvtr(Idx: Integer);
|
||
procedure DeactivateXvtr;
|
||
|
||
// Полный HP-кадр в радио (RX/TX freq через трансвертор + drive + MOX).
|
||
// Вызывается командами смены частоты/центра и UI после правки drive.
|
||
procedure PushNetworkState;
|
||
|
||
// ====================================================================
|
||
// КОМАНДЫ — единый API для GUI / Web / CAT / Arduino.
|
||
// Фаза 1: обновляют поле + шлют событие; логика движка — TODO-wiring.
|
||
// ====================================================================
|
||
|
||
// Частоты / VFO
|
||
procedure SetVfoA(Hz: Double);
|
||
procedure StoreVfoA(Hz: Double); // сохранить частоту A без перестройки (когда активен B)
|
||
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 SetRawFMDeviation(Hz: Integer);
|
||
procedure SetAGCMode(M: Integer);
|
||
procedure SetAGCTop(DB: Integer);
|
||
procedure AGCTopBy(Delta: Integer);
|
||
|
||
// RX hw-gain (Pluto): режим (0=manual,1=fast,2=slow,3=hybrid) / ручной дБ.
|
||
// На HPSDR — no-op в бэкенде.
|
||
procedure SetRxGainMode(M: Integer);
|
||
procedure SetRxGain(Db: Integer);
|
||
procedure RxGainBy(Delta: Integer);
|
||
procedure ApplyRxGainToBackend; // FRxGain* → FNetwork.SetRxGain
|
||
|
||
// Громкость / mute / drive / аттенюатор
|
||
procedure SetVolume(V: Integer);
|
||
procedure VolumeBy(Delta: Integer);
|
||
// Громкость self-monitor'а на передаче отдельно от RX-громкости: слайдер
|
||
// правит её только когда MonitoringTX, а CAT (ZZTM) — в любой момент.
|
||
{ Адресная громкость ПРИЁМА: правит FVolume независимо от того, идёт ли
|
||
передача. Слайдер (SetVolume) правит активную — на self-monitor это
|
||
громкость монитора; внешнему клиенту (TCI VOLUME) такой контекст не нужен. }
|
||
procedure SetRxVolume(V: Integer);
|
||
procedure SetTXMonVolume(V: Integer);
|
||
// True — сейчас звучит self-monitor даунлинка на передаче (TX + DUP + RX MUTE
|
||
// off). В этом контексте SetVolume/слайдер правят FTxMonVolume, иначе FVolume.
|
||
function MonitoringTX: Boolean;
|
||
// Активная громкость под текущий контекст (RX или TX-монитор) — то, что видит
|
||
// слайдер (rfVolume) и получает движок.
|
||
function ActiveVolume: Integer;
|
||
// Применить активную громкость к движку + синхронизировать слайдер (rfVolume).
|
||
// Зовётся на T/R-переходах и смене RX MUTE / DUP.
|
||
procedure ApplyActiveVolume;
|
||
procedure SetMute(On_: Boolean);
|
||
procedure ToggleMute;
|
||
procedure SetDrive(V: Integer);
|
||
procedure DriveBy(Delta: Integer);
|
||
procedure SetAtten(Db: Integer); // ADC step attenuator, дБ (0..31)
|
||
|
||
// ---- Мультислайсы (софт-fan-out) ----
|
||
// Создаёт слайс на TargetHz: заводит WDSP-канал в движке + свой аудио-выход
|
||
// на устройстве DevIndex (-1=default). Возвращает Id слайса или -1 при отказе
|
||
// (нет слотов / TargetHz вне полосы пана). DevName для персиста/UI.
|
||
// APanId>0 — слайс внутри доп. DDC-пана (тот должен быть создан AddPanDDC).
|
||
function AddSlice(TargetHz: Double; Mode, FilterLo, FilterHi: Integer;
|
||
AGC: TWDSPAGCMode; Vol: Double;
|
||
DevIndex: Integer; const DevName: string;
|
||
APanId: Integer = 0): Integer;
|
||
procedure RemoveSlice(Id: Integer);
|
||
procedure SetSliceTarget(Id: Integer; TargetHz: Double);
|
||
procedure SetSliceMode(Id, Mode: Integer);
|
||
procedure SetSliceFilter(Id, Low, High: Integer);
|
||
procedure SetSliceModeBW(Id, Mode, BW: Integer); // режим+полоса → кромки фильтра
|
||
class procedure FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
||
// --- Редактируемая таблица фильтров (пресеты + VAR1/VAR2 на режим) ---
|
||
function FilterSlot(Mode, Idx: Integer): TFilterSlot;
|
||
procedure SetFilterSlot(Mode, Idx: Integer; const S: TFilterSlot);
|
||
procedure ResetFilterSlot(Mode, Idx: Integer);
|
||
procedure ResetFilterSet(Mode: Integer);
|
||
// Произвольные кромки (drag на спектре, CAT ZZFL/ZZFH, web): пишутся в
|
||
// VAR1 и переключают на него, если текущий слот — пресет. Пресеты таким
|
||
// образом невозможно испортить случайным движением мыши.
|
||
procedure SetFilterEdges(LoHz, HiHz: Integer);
|
||
// Только state, без применения к тракту: UpdateFilterButtons — рендер,
|
||
// DSP в его путях перестраивается отдельно (как было до таблицы).
|
||
procedure SelectFilterSlotState(Idx: Integer);
|
||
procedure SanitizeFilterState;
|
||
// Знаковые края TX-полосы (для отрисовки полосы фильтра при передаче).
|
||
procedure TXFilterEdges(out Lo, Hi: Integer);
|
||
property FilterLo: Integer read FFilterLo;
|
||
property FilterHi: Integer read FFilterHi;
|
||
class function AGCModeFromUI(M: Integer): TWDSPAGCMode; // UI-индекс 0..4 → WDSP enum
|
||
class function AGCModeToUI(A: TWDSPAGCMode): Integer; // и обратно
|
||
procedure SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
|
||
procedure SetSliceVolume(Id: Integer; Vol: Double); // 0..1
|
||
procedure SetSliceMute(Id: Integer; Mute: Boolean);
|
||
// Слайс слушает внешний потребитель (поток RX_AUDIO по TCI): молчащий в
|
||
// комнате слайс обязан считаться, пока его кто-то качает по сети. Зеркала
|
||
// в TCtrlSlice нет намеренно — флаг принадлежит владельцу потоков, а не
|
||
// модели, и пересоздание движка слайсы всё равно не переживают.
|
||
procedure SetSliceTapWanted(Id: Integer; V: Boolean);
|
||
procedure SetSliceRxMuteOnTx(Id: Integer; On_: Boolean); // self-monitor слайса на TX
|
||
function PanPrimarySliceId(PanId: Integer): Integer; // первый созданный слайс пана
|
||
function PanHasFMSlice(PanId: Integer): Boolean; // есть слайс в канальном режиме (FM/DMR/FMRAW)?
|
||
procedure SetSliceDSP(Id, NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
||
procedure SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer);
|
||
function SliceSMeter(Id: Integer): Double; // dBm, из DSP-потока
|
||
// S-метр главного приёмника, dBm, с калибровочным офсетом текущего
|
||
// диапазона/трансвертера (движок отдаёт сырое значение).
|
||
function ReadSMeterDBm: Double;
|
||
// Офсет S-метра (дБ) для видимой частоты: слот трансвертера или диапазон.
|
||
function SMeterCalFor(VisibleHz: Double): Double;
|
||
// Офсет спектра/водопада (дБ) для дисплея: PanId 0 = главный, 1..N — пан.
|
||
// Колбэк движка (display-поток), см. TOnDispCalQuery.
|
||
function DispCalFor(PanId: Integer): Double;
|
||
// Поправка на регулируемое усиление входа (аттенюатор ADC у openHPSDR,
|
||
// hardwaregain у AD936x) — входит в оба офсета выше.
|
||
function FrontEndCalDB: Double;
|
||
procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string);
|
||
// Устройство ВВОДА слайса (источник модуляции, когда слайс = TX-источник).
|
||
// DevIndex=-1 / DevName='' — вернуться на общий вход (FAudioIn).
|
||
procedure SetSliceInDevice(Id, DevIndex: Integer; const DevName: string);
|
||
// Общее устройство ввода (главный VFO = слайс A + фолбэк для слайсов).
|
||
procedure SetMainMicDevice(DevIndex: Integer; const DevName: string);
|
||
// Поток/имя устройства ввода активного TX-источника (слайс или общий).
|
||
function ActiveMicInput: TAudioInput;
|
||
function ActiveMicInDevName: string;
|
||
function SliceCount: Integer;
|
||
function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
|
||
// Снимок слайса для фронтендов, читающих модель из СВОИХ потоков (TCI).
|
||
// См. TSliceView: копировать целиком TCtrlSlice оттуда нельзя.
|
||
function GetSliceView(Id: Integer; out V: TSliceView): Boolean;
|
||
// Слайсы пана по порядку (0-й = «канал A» пана): та же нумерация, что у
|
||
// флагов. Нужны TCI, где приёмник — это пан, а канал — слайс на нём.
|
||
function PanSliceCount(PanId: Integer): Integer;
|
||
function PanSliceId(PanId, Index: Integer): Integer; // 0 = нет такого
|
||
function SlicePanIndex(Id: Integer; out PanId, Index: Integer): Boolean;
|
||
// Слот ↔ слайс. Слот 0..MAX_SLICES-1 = буква B..G и не зависит от того,
|
||
// создан ли слайс сейчас: на слот вешаются настройки (CAT-порт, Auto TX).
|
||
class function SliceSlotLetter(Slot: Integer): Char;
|
||
function SliceIdBySlot(Slot: Integer): Integer; // 0 = слот пуст
|
||
function SliceSlotOf(Id: Integer): Integer; // -1 = нет такого слайса
|
||
// Auto TX слайса (см. FSliceAutoTx). Геттер по слоту — для UI настроек,
|
||
// по Id — для флага слайса и CAT.
|
||
function SliceSlotAutoTx(Slot: Integer): Boolean;
|
||
procedure SetSliceSlotAutoTx(Slot: Integer; On_: Boolean);
|
||
function SliceAutoTx(Id: Integer): Boolean;
|
||
// Аудио-устройства слота. Сеттер применяет их к слайсу, который стоит на
|
||
// слоте прямо сейчас (имя → PA-индекс текущей сессии); '' = общий.
|
||
function SliceSlotOutDev(Slot: Integer): string;
|
||
function SliceSlotInDev(Slot: Integer): string;
|
||
procedure SetSliceSlotAudio(Slot: Integer; const OutDev, InDev: string);
|
||
// Заявка на передачу ОТ ИМЕНИ слайса (CAT-порт слайса, TCI с номером
|
||
// приёмника). Единственная точка, где внешняя PTT может сменить TX-источник,
|
||
// поэтому здесь же — защита «в эфире одновременно только один слайс».
|
||
// Tune = True — то же самое, но несущей настройки (TUN) вместо модуляции.
|
||
// Выполнять в потоке контроллера.
|
||
procedure RequestSliceTx(SliceId: Integer; On_: Boolean;
|
||
Tune: Boolean = False);
|
||
// Диапазон захваченной полосы пана (0 = главный), куда можно ставить слайс.
|
||
procedure CapturedSpan(out LoHz, HiHz: Double; PanId: Integer = 0);
|
||
function SliceFitsCapture(TargetHz: Double; PanId: Integer = 0): Boolean;
|
||
// Границы ВКЛЮЧЁННОГО сейчас диапазона (трансвертер — его FreqBegin/FreqEnd,
|
||
// иначе band-план бэкенда). False = диапазон неизвестен.
|
||
function CurrentBandEdges(out LoHz, HiHz: Double): Boolean;
|
||
// Перестроить слайс на TargetHz В ПРЕДЕЛАХ текущего диапазона: если частота
|
||
// вне захваченной полосы пана — двигаем окно DDC. False = отказ (другой
|
||
// диапазон либо окно не подвинуть, не оглушив главный приёмник).
|
||
function TuneSliceInBand(Id: Integer; TargetHz: Double): Boolean;
|
||
// Уведомление UI «частота слайса пришла извне»: перезалить флаг (цифры +
|
||
// кромки фильтра), позиция флага и так живая.
|
||
procedure SliceFreqChanged(Id: Integer);
|
||
procedure SliceStateChanged(Id: Integer);
|
||
|
||
// ---- Панадаптеры на аппаратных DDC (этап 3.1; UI/движок — 3.2/3.3) ----
|
||
// Создаёт пан PanId (1..MAX_PANS-1) на своём DDC: FreqHz = центр в ВИДИМЫХ
|
||
// Гц (как FVfoA/FCenterFreq: при активном XVTR транслируется в IF только
|
||
// на границе с железом, XvtrTranslate), RateKHz ∈ PanRateAllowed (иначе
|
||
// 384), ADCSource — АЦП платы. False = нет слота/бэкенд не умеет/не Running.
|
||
function AddPanDDC(PanId: Integer; FreqHz: Double; RateKHz: Word = 384;
|
||
ADCSource: Byte = 0): Boolean;
|
||
procedure RemovePanDDC(PanId: Integer);
|
||
procedure SetPanDDCFreq(PanId: Integer; FreqHz: Double); // ретюн (HP-пакет)
|
||
// Допустимое окно ВИДИМОЙ частоты пана: активный XVTR — его диапазон,
|
||
// иначе пределы железа (UI-кламп drag/тюна панов N).
|
||
procedure VisibleFreqBounds(out LoHz, HiHz: Double);
|
||
// Вход/выход/перекалибровка XVTR при живых панах: клампит видимые частоты
|
||
// панов в новое окно, перешивает freq-слова DDC (XvtrTranslate), слайсы
|
||
// панов — к краям их полос захвата.
|
||
procedure RefreshPanDDCs;
|
||
// Rate ∈ сетке бэкенда (Caps.RatePresets) и кратен 48 кГц (движок).
|
||
function PanRateAllowed(RateKHz: Word): Boolean;
|
||
// Смена rate живого пана: движок переоткрывает аккумулятор/анализатор/слайсы,
|
||
// DDC реконфигурируется; слайсы вне новой полосы клампятся к её краям.
|
||
function SetPanDDCRate(PanId: Integer; RateKHz: Word): Boolean;
|
||
// Переезд пана на другой аппаратный DDC (пан/слайсы/движок не пересоздаются,
|
||
// меняется только hw-индекс): бывает, что конкретный DDC платы ведёт себя
|
||
// хуже соседей — тогда пан можно просто увести на другой.
|
||
function MovePanDDC(PanId, NewDDC: Integer): Boolean;
|
||
function PanDDCIndex(PanId: Integer): Integer; // hw-индекс DDC пана
|
||
function MaxDDCIndex: Integer; // последний DDC железа
|
||
function DDCFree(DDCIdx: Integer): Boolean; // индекс свободен?
|
||
function PanDDCActive(PanId: Integer): Boolean;
|
||
function PanDDCFreq(PanId: Integer): Double;
|
||
function PanDDCRateKHz(PanId: Integer): Word;
|
||
// ADC-источник пана (3.4). PanId=0 = главный DDC (реконфиг ConfigureDDCs),
|
||
// N>0 = его пан-DDC (SetPanDDC). ADC ∈ [0..Caps.NumADCs-1].
|
||
// Антенные разъёмы AD936x: настройки (вкладка Antenna) + применение текущего
|
||
// контекста (диапазон | активный трансвертер) к железу.
|
||
procedure SetAnt936x(const A: TAD936xAnt);
|
||
procedure ApplyAntPorts;
|
||
property Ant936x: TAD936xAnt read FAnt936x;
|
||
function PanADC(PanId: Integer): Byte;
|
||
function SetPanADC(PanId: Integer; ADC: Byte): Boolean;
|
||
// ---- Зум/пан доп. панов (PanId=0 делегирует главному зуму) ----
|
||
// Видимое окно пана — та же формула, что span-clip его анализатора.
|
||
procedure GetPanViewWindow(PanId: Integer; out ACenter, ASpan: Double);
|
||
procedure SetPanZoomPan(PanId: Integer; AZoom, APan: Double);
|
||
procedure PanZoomIn(PanId: Integer); // шаг по лестнице, якорь = центр окна
|
||
procedure PanZoomOut(PanId: Integer);
|
||
procedure PanZoomDefault(PanId: Integer);
|
||
function PanZoomFactor(PanId: Integer): Double;
|
||
function PanPanSlider(PanId: Integer): Double;
|
||
|
||
// Pluto TX-мощность через аттенюацию: drive% → дБ (потолок FPlutoTxMaxAttDb на
|
||
// 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain.
|
||
function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent
|
||
procedure ApplyPlutoTxAtten; // ставит att по FTransmitting/FTuning
|
||
procedure SetPlutoTxMaxAtt(Db: Double); // потолок на drive=100% (внешний усилитель)
|
||
procedure SetCalibration(const C: TCalibration); // вкладка Calibration → FCal (офсеты S-метра — в SMeterCalFor)
|
||
procedure ApplyWDSPDrive(Pct: Integer); // цифровой drive WDSP: Pluto=1.0 (мощность — атт), HPSDR=Pct/100
|
||
|
||
// Диапазоны / трансвертеры / спан
|
||
procedure SetBand(Idx: Integer);
|
||
procedure BandUp;
|
||
procedure BandDown;
|
||
procedure SetXvtrBand(Idx: Integer); // -1 = выход в HF
|
||
procedure SetCurrentBandIdx(NewBand: Integer); // лёгкая смена HF-диапазона (band+drive+рендер), без restore
|
||
procedure SetSpan(Hz: Integer);
|
||
procedure SetSampleRate(Hz: Integer);
|
||
|
||
// ---- Zoom/pan спектра и водопада ----
|
||
// GetViewWindow — единая истина «видимого окна» (центр+span в Гц) для всех
|
||
// оверлеев и маппинга мыши: при зуме отдаёт окно из WDSP-анализатора, иначе
|
||
// базовые FCenterFreq/FSpanHz.
|
||
procedure GetViewWindow(out ACenter: Double; out ASpan: Double);
|
||
procedure ApplyZoom(AZoom, APan: Double); // задать зум+пан, сохранить в band, Changed(rfZoom)
|
||
procedure ZoomIn; // шаг увеличения (якорь — маркер активного VFO)
|
||
procedure ZoomOut; // шаг уменьшения
|
||
procedure ZoomDefault; // сброс в 1.0 (весь span)
|
||
function ZoomFactor: Double;
|
||
function PanSlider: Double;
|
||
|
||
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
|
||
// QO-100 beacon lock (= декодер + трим LOError; декодер = эталон частоты).
|
||
procedure SetBeaconLock(On_: Boolean); // вкл/выкл лок (запускает декодер)
|
||
procedure ServiceBeaconLock; // одна итерация контура (MeterTimer @10 Гц)
|
||
procedure SetBeaconRefHz(Hz: Double); // опорная частота маяка (downlink)
|
||
procedure BeaconSeedAtHz(DisplayHz: Double); // клик по маяку → наведение декодера
|
||
function BeaconState: TBeaconState;
|
||
function BeaconCorrectionHz: Double; // коррекция за сессию лока (индикация; в LOError)
|
||
function BeaconSNRdB: Double;
|
||
function BeaconLockEnabled: Boolean;
|
||
function BeaconRefHz: Double; // опорная частота маяка (для маркера)
|
||
function BeaconTrackedFreqHz: Double;// измеренная display-частота маяка (наведение+остаток)
|
||
function BeaconDecodeFreqHz: Double; // частота наведения (для узкого маркера)
|
||
procedure SetBeaconDecodeInvert(On_: Boolean); // зеркальный baseband вкл/выкл
|
||
function BeaconDecodeInvert: Boolean;
|
||
function GetBeaconScope(out S: TBeaconScope): Boolean;
|
||
function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
|
||
// --- Лупа маяка (окно наведения в BeaconScopeForm) -------------------
|
||
// Отдельный WDSP-анализатор на IQ главного тракта, вырезающий узкое окно
|
||
// вокруг маяка: на общей спектрограмме 576к маяк — пара пикселей, точно
|
||
// ткнуть в него нельзя. Все частоты — АБСОЛЮТНЫЕ display-Гц, как у
|
||
// BeaconSeedAtHz. Зовётся с таймера окна маяка; окно закрыто → Enabled=False
|
||
// и анализатор уничтожается.
|
||
procedure SetBeaconZoomWindow(AEnabled: Boolean; ACenterHz, ASpanHz: Double);
|
||
function GetBeaconZoomSpectrum(var Pixels: array of Single;
|
||
out Count: Integer; out LowHz, HighHz: Double): Boolean;
|
||
function GetBeaconZoomWaterfall(var Pixels: array of Single;
|
||
out Count: Integer): Boolean;
|
||
// Полоса захвата (display-Гц): за её пределы лупу не увести.
|
||
procedure GetCaptureWindow(out ALowHz, AHighHz: Double);
|
||
function BeaconZoomBinHz: Double; // разрешение лупы, Гц/бин (для подписи)
|
||
function GetDMRStatus(out S: TDMRStatus): Boolean;
|
||
function GetSliceDMRStatus(SliceId: Integer; out S: TDMRStatus): Boolean;
|
||
|
||
// Отображение / приём
|
||
procedure SetCTun(On_: Boolean);
|
||
procedure SetDuplex(On_: Boolean);
|
||
procedure SetRxMuteOnTx(On_: Boolean); // QO-100 self-monitor (кнопка RX MUTE)
|
||
procedure ApplyRxMuteToEngine; // FRxMuteOnTx → FDSPEngine.MonitorRXDuringTX
|
||
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
|
||
|
||
// ---- Телеграф ----------------------------------------------------------
|
||
// Настройки применяются целиком: pitch уезжает в движок (там он двигает
|
||
// гетеродин и полосу), остальное — в DUC Specific прошивке.
|
||
// «Железо сейчас в эфире» — ДЛЯ ИНДИКАЦИИ (S-метр, лампочки), не для тракта.
|
||
// В телеграфе FTransmitting не поднимается: передачей владеет кейер прошивки,
|
||
// и единственный признак эфира — HPS_PTT в статусе.
|
||
function RadioKeyed: Boolean;
|
||
// Фронты RadioKeyed → индикация. Зовётся из разбора HP-статуса (openHPSDR) и
|
||
// с UI-таймера: у Pluto статуса нет вовсе, а телеграф там локальный.
|
||
procedure ServiceCWKeyed;
|
||
function CWSettings: TCWSettings;
|
||
procedure SetCWSettings(const C: TCWSettings);
|
||
// Программная передача текста: PC сам выдерживает тайминги и дёргает бит
|
||
// CWX в High Priority — точки/тире рисует прошивка по этому биту (эталон
|
||
// Thetis cwx.cs). Занят ли передатчик — CWXBusy; ЛЮБОЕ касание
|
||
// манипулятора или снятие MOX обрывает передачу (CWXAbort).
|
||
procedure CWXSend(const Text: string);
|
||
// Чужая манипуляция: один готовый интервал (посылка или пауза) заданной
|
||
// длины. Так приходит телеграф по TCI (команда KEYER) — знаков там нет,
|
||
// есть измеренные клиентом длительности. См. TCWElemPlayer.
|
||
procedure CWKeyerElement(Mark: Boolean; Ms: Integer);
|
||
procedure CWXAbort;
|
||
function CWXBusy: Boolean;
|
||
// ---- Окно-терминал -----------------------------------------------------
|
||
// Стереть последний НЕ ушедший в эфир знак (Backspace при наборе).
|
||
function CWXBackspace: Boolean;
|
||
// Что ещё стоит в очереди передачи (строка набора показывает именно это).
|
||
function CWXPendingText: string;
|
||
// Ключ с клавиатуры: при иамбике — лепестки, при прямом ключе — уровень.
|
||
// На кейере прошивки лепестков нет (их читает FPGA со своего разъёма),
|
||
// поэтому там клавиатура работает через бит CWX, то есть прямым ключом.
|
||
procedure CWKeyboardKey(Dot, Dash: Boolean);
|
||
// Разбор принимаемого телеграфа: включение и обслуживание (таймер хозяина).
|
||
procedure SetCWDecoder(On_: Boolean);
|
||
function CWDecoderOn: Boolean;
|
||
procedure ServiceCWDecoder;
|
||
// Последние знаки ленты (для полосы под спектром) и счётчик изменений.
|
||
function CWLogTail(MaxChars: Integer): string;
|
||
property CWLogSeq: Int64 read FCWLogSeq;
|
||
// Оценка скорости корреспондента и расстройка тона (индикация в терминале).
|
||
function CWDecSpeedWPM: Integer;
|
||
function CWDecToneOffsetHz: Integer;
|
||
function CWDecSignal: Boolean;
|
||
property OnCWLog: TCWLogEvent read FOnCWLog write FOnCWLog;
|
||
|
||
// ---- PureSignal ----
|
||
// Доступность PS у активного бэкенда/платы (Caps.HasPureSignal).
|
||
function PSAvailable: Boolean;
|
||
// Вкл/выкл авто-калибровки PureSignal (кнопка PS). Вооружает feedback-тракт
|
||
// (DDC0/1 на TX) и WDSP calcc automode; выкл = reset коррекции.
|
||
procedure SetPureSignal(On_: Boolean);
|
||
procedure PSSingleCal; // одиночная калибровка
|
||
procedure PSReset; // выключить и сбросить коррекцию
|
||
// Тик PS (~10 Гц, из UI-таймера): state machine команд (порт Thetis
|
||
// PSForm timer1/timer2), опрос GetPSInfo, auto-attenuate через AttOnTX.
|
||
procedure PureSignalTick;
|
||
procedure ApplyPSSettingsToDSP; // FTXSettings.PS* → WDSP calcc
|
||
// Вооружение/снятие feedback-тракта (сеть + движок), идемпотентно.
|
||
procedure PSSetFeedbackPath(En: Boolean);
|
||
// Статус для UI (кэш последнего PureSignalTick)
|
||
function PSFeedbackLevel: Integer; // info[4], оптимум ~PSFBTarget
|
||
// Эффективная цель уровня feedback: настройка ps_fb_target, либо авто по
|
||
// плате (Orion MkII/ANAN-7000/8000 → 22 — выше перегружается вход ADC,
|
||
// замеры eu2av на Anvelina PRO3; остальные → 152, классика Thetis).
|
||
function PSFBTarget: Integer;
|
||
function PSCalCount: Integer; // info[5]
|
||
function PSCorrecting: Boolean; // info[14]=1 — коррекция применяется
|
||
function PSMaxTXVal: Double; // GetPSMaxTX (пик TX для SetPk)
|
||
function PSSingleCalActive: Boolean; // идёт одиночная калибровка
|
||
|
||
// 2TON: двухтональник (PostGen two-tone) + MOX — тест IMD / проверка PS.
|
||
procedure SetTwoTone(On_: Boolean);
|
||
|
||
procedure SetSplit(TxOnB: Boolean); // split: TX на VFO-B (иначе VFO-A)
|
||
procedure SetVfoLock(On_: Boolean); // блокировка перестройки VFO
|
||
|
||
// 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
|
||
|
||
// Каналы: применение/деактивация каналов вынесено в TChannelController
|
||
// (отдельный юнит-адаптер поверх контроллера), оркеструет команды ниже.
|
||
|
||
// Жизненный цикл движка / устройства
|
||
procedure StartStop;
|
||
procedure SetRun(On_: Boolean);
|
||
procedure StartRunning(SpectrumWidth: Integer); // Run=1 bring-up: General/DDC/DUC + DSP restore
|
||
procedure StopRunning; // Run=0: graceful-стоп RX-потока + сброс TX/телеметрии
|
||
function ConnectDevice(const Dev: THPSDRDevice): Boolean; // Connect + load-by-MAC; рендер через rfDevice
|
||
function ConnectAndRun(const Dev: THPSDRDevice; SpectrumWidth: Integer): Boolean; // ConnectDevice + band/xvtr/vfo restore + go-live
|
||
procedure StopAndDisconnect; // graceful Run=0 + персист настроек + Disconnect (headless)
|
||
// По IP/URI ищет устройство в store (discovered→saved), восстанавливая
|
||
// Kind/URI/Serial/BoardType/MAC. Нужно для Pluto (открывается по URI, выбор
|
||
// бэкенда — по Kind). Не найдено → простой HPSDR по IP. Общий путь GUI+демон.
|
||
function ResolveDevice(const IP: string): THPSDRDevice;
|
||
// Добирает MAC устройства, если он неизвестен (старт по сохранённому IP /
|
||
// AutoStart идёт без дискавери). MAC — ключ профиля настроек, без него
|
||
// настройки уезжают в «нулевой» профиль. Источники по порядку: список
|
||
// найденного в этой сессии → MAC из hpsdr_devices.ini → короткий поиск в
|
||
// сети (ответ discovery несёт MAC). False = добыть не удалось.
|
||
function EnsureDeviceMac(var Dev: THPSDRDevice): Boolean;
|
||
// Сохраняет устройство по введённому адресу. Схема URI ('ip:'/'usb:'/'local:')
|
||
// → Pluto (AddSavedPluto), иначе HPSDR-IP. Общий путь web-dev_add для GUI и
|
||
// демона (десктоп-диалог сохраняет Pluto своим путём через AddSavedPluto).
|
||
procedure AddSavedDevice(const AName, AAddr: string);
|
||
procedure Discover;
|
||
// Гарантирует, что FNetwork — бэкенд нужного типа (HPSDR/Pluto): при
|
||
// несовпадении пересоздаёт его, перенося колбэки данных. Вызывается из
|
||
// ConnectDevice по Dev.Kind. Discovery использует транзиентные экземпляры.
|
||
procedure EnsureBackend(Kind: TBackendKind);
|
||
// Возможности текущего бэкенда (для гейтинга UI / пресетов sample rate).
|
||
function BackendCaps: TBackendCaps;
|
||
// Единый источник пресетов sample-rate для UI (desktop overlay + web).
|
||
// Берётся из caps текущего бэкенда (HPSDR 48k..1536k / Pluto 576k..5760k).
|
||
function SampleRatePresets: TBackendRateArray;
|
||
// Единый источник band-плана для UI (HF / VHF-UHF по типу бэкенда).
|
||
function BandPlan: TBandPlanArray;
|
||
// True если активный бэкенд — Pluto (VHF/UHF band-план вместо HF).
|
||
function IsPluto: Boolean;
|
||
// True — активен QO-100 (Pluto + full-duplex XVTR-слот); гейт UI beacon.
|
||
function InQO100: Boolean;
|
||
// True — включён режим DUP (дисплей+RX держатся на TX). Гейт активности
|
||
// кнопки RX MUTE (self-monitor) на ЛЮБОМ бэкенде — без DUP звук на TX не идёт.
|
||
function DisplayDuplex: Boolean;
|
||
// Человекочитаемое имя подключённой платы для статус-строки (UI/Web).
|
||
// HPSDR → тип платы; Pluto/LibreSDR → hw_model + серийник (если есть).
|
||
function BoardDisplayName: string;
|
||
// Опрос аппаратной телеметрии Pluto (температура/RSSI) с троттлингом — зовётся
|
||
// с UI-таймера (MeterTimer @10 Гц), как ServiceBeaconLock. No-op для HPSDR.
|
||
// Результат — в FLastPlutoTempC/FLastPlutoRSSIdB (UI читает и форматирует).
|
||
procedure ServicePlutoTelemetry;
|
||
// Список URI для probe сетевых Pluto (mDNS недоступен) — задаёт фронтенд
|
||
// перед Discover (сохранённые Pluto + ручной ввод IP).
|
||
procedure SetPlutoProbeURIs(const URIs: array of string);
|
||
// Сидирует probe-список из сохранённых Pluto (для бэкенд-агностичного Discover
|
||
// без UI-диалога — демон). GUI добавляет ручной IP поверх в BuildProbeURIs.
|
||
procedure SeedPlutoProbeFromSaved;
|
||
|
||
// ---- События наружу ----
|
||
property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke;
|
||
property OnStateChanged: TRadioStateEvent read FOnStateChanged write FOnStateChanged;
|
||
// Доп. наблюдатели состояния (помимо единственного OnStateChanged, который
|
||
// занимает MainForm). Нужны CAT/Andromeda для push индикаторов панели.
|
||
procedure AddStateListener(L: TRadioStateEvent);
|
||
// Снять наблюдателя. Обязателен для тех, кто умирает раньше контроллера
|
||
// (TCI-адаптер): иначе Changed() позовёт метод освобождённого объекта.
|
||
procedure RemoveStateListener(L: TRadioStateEvent);
|
||
// Пост-тюн хук: вызывается в КОНЦЕ SetVfoA (после Changed(rfVfoA), т.е. вне
|
||
// рендера) — сюда вешается канальная оркестрация (ChannelController.OnVfoTuned),
|
||
// чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless.
|
||
property OnAfterTune: TThreadMethod read FOnAfterTune write FOnAfterTune;
|
||
property OnBeforeStop: TThreadMethod read FOnBeforeStop write FOnBeforeStop;
|
||
property OnAudioConsume: TAudioConsumeEvent read FOnAudioConsume write FOnAudioConsume;
|
||
// Тапы RX-аудио: слушают, ничего не забирая (см. TRadioAudioTapEvent).
|
||
// Снимать обязан тот, кто умирает раньше контроллера.
|
||
procedure AddAudioTap(T: TRadioAudioTapEvent);
|
||
procedure RemoveAudioTap(T: TRadioAudioTapEvent);
|
||
// Тап сырого RX-IQ (потоки IQ по TCI). Пас в движок: он же и владелец
|
||
// потока, из которого тап зовётся. nil — снять.
|
||
procedure SetIQTap(T: TIQTapEvent);
|
||
// Частота дискретизации источника IQ для приёмника: главный тракт — rate
|
||
// устройства, доп. пан — rate его DDC.
|
||
function IQTapRateHz(PanId: Integer): Integer;
|
||
// TX-аудио от TCI-клиента: 48 кГц моно в тот же ринг, что и web-микрофон.
|
||
procedure PushTCIAudio(const Samples: array of Double; N: Integer);
|
||
// Локальный звук ПК (динамик RX / микрофон TX). Демон ставит False: аудио
|
||
// только через web (OnAudioConsume), локальная звуковая карта не трогается.
|
||
property LocalAudioEnabled: Boolean read FLocalAudio write FLocalAudio;
|
||
// Клиент TCI просит брать модуляцию из своего аудиопотока (TRX:0,true,tci,
|
||
// §4.2). Ставит адаптер, читает SetMOX при выборе источника.
|
||
property TCIMicRequested: Boolean read FTCIMicRequested write FTCIMicRequested;
|
||
// Сколько звука клиент TCI держит в авансе (отсчётов 48 кГц). Ставит
|
||
// адаптер по измеренной зернистости клиента; SetMOX по этому числу
|
||
// растягивает нулевой pre-roll DUC.
|
||
property TCITxLeadFrames: Integer read FTCITxLeadFrames write FTCITxLeadFrames;
|
||
// Идёт ли текущая передача с модуляцией из TCI (адаптеру — гнать ли
|
||
// маркеры TX_CHRONO).
|
||
property TCIMicActive: Boolean read FTCIMicActive;
|
||
property OnSpectrumData: TPixelDataEvent read FOnSpectrumData write FOnSpectrumData;
|
||
property OnWaterfallData: TPixelDataEvent read FOnWaterfallData write FOnWaterfallData;
|
||
end;
|
||
|
||
implementation
|
||
|
||
const
|
||
// Амплитуда TUN-тона (WDSP PostGen). НЕ 1.0: на резком старте тона фильтры
|
||
// TXA-тракта дают переходный выброс ~×1.18 (замерено: peak=1.176 при
|
||
// mag=1.0) поверх полной шкалы → жёсткий кламп 24-бит → сплэттер на старте
|
||
// TUN. 0.83×1.18≈0.98 — выброс остаётся в шкале. Мощность TUN задаёт drive
|
||
// (TUNLevel), падение уровня IQ ~1.6дБ компенсируется калибровкой TUNLevel.
|
||
TUN_TONE_MAG = 0.83;
|
||
|
||
// Состояния командной state machine PureSignal (порт Thetis eCMDState;
|
||
// Restore-ветка (7) не реализована — Save/Restore коррекции нет).
|
||
PS_ST_OFF = 0;
|
||
PS_ST_TURNON_AUTO = 1;
|
||
PS_ST_AUTOCAL = 2;
|
||
PS_ST_TURNON_SINGLE = 3;
|
||
PS_ST_SINGLECAL = 4;
|
||
PS_ST_STAYON = 5;
|
||
PS_ST_TURNOFF = 6;
|
||
|
||
constructor TDMRSliceDecoder.Create(AOwner: TRadioController;
|
||
ASliceId: Integer);
|
||
begin
|
||
inherited Create;
|
||
FOwner := AOwner;
|
||
FSliceId := ASliceId;
|
||
FDecoder := TDMRDecoder.Create;
|
||
FDecoder.SetInverted(True);
|
||
FDecoder.OnAudio := AudioReady;
|
||
FDecoder.SetEnabled(True);
|
||
end;
|
||
|
||
destructor TDMRSliceDecoder.Destroy;
|
||
begin
|
||
if Assigned(FDecoder) then
|
||
begin
|
||
FDecoder.SetEnabled(False);
|
||
FDecoder.OnAudio := nil;
|
||
FreeAndNil(FDecoder);
|
||
end;
|
||
FOwner := nil;
|
||
inherited Destroy;
|
||
end;
|
||
|
||
procedure TDMRSliceDecoder.FeedAudio(const Left, Right: array of Single;
|
||
Count: Integer);
|
||
begin
|
||
if Assigned(FDecoder) then FDecoder.FeedAudio(Left, Right, Count);
|
||
end;
|
||
|
||
procedure TDMRSliceDecoder.GetStatus(out S: TDMRStatus);
|
||
begin
|
||
if Assigned(FDecoder) then FDecoder.GetStatus(S)
|
||
else FillChar(S, SizeOf(S), 0);
|
||
end;
|
||
|
||
procedure TDMRSliceDecoder.AudioReady(const PCM: array of Single;
|
||
Count: Integer);
|
||
begin
|
||
if Assigned(FOwner) then FOwner.OnSliceDMRAudioReady(FSliceId, PCM, Count);
|
||
end;
|
||
|
||
constructor TRadioController.Create;
|
||
begin
|
||
inherited Create;
|
||
FSettings := TSettingsManager.Create; // владелец настроек (GUI и демон)
|
||
FDeviceStore := TDeviceStore.Create; // общий список устройств (desktop+web)
|
||
FAudioTapLock := TCriticalSection.Create;
|
||
FAudioTapCount := 0;
|
||
// Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны).
|
||
FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0;
|
||
FMode := MODE_USB; FFilter := 5;
|
||
DefaultFilterTable(FFilterTable); // до подключения — заводские фильтры
|
||
ApplyFilterSlot(FFilter);
|
||
FAGCMode := 1; FAGCTop := 90;
|
||
TSettingsManager.DefaultCW(FCWSettings); // до подключения — заводской телеграф
|
||
FLastCWOptions := $FF; // «ещё не слали» — первый пуш пройдёт
|
||
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
|
||
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
|
||
FVolume := 70; FTxMonVolume := 40; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
|
||
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
|
||
FNextSliceId := 1; // мультислайсы: генератор id
|
||
FillChar(FSlices, SizeOf(FSlices), 0);
|
||
ResetPanDDCMaps; // паны: все слоты свободны
|
||
FZoomFactor := 0.0; FPanSlider := 0.5; // 0.0 = весь span (без зума)
|
||
FCurrentBand := 5; FCurrentXvtr := -1;
|
||
// Кадра ещё не было — 0 точек. Ненулевой счётчик заставил бы web рисовать
|
||
// нулевой (= 0 dBm, полная шкала) буфер белой полосой.
|
||
FSpectrumBufCount := 0; FWaterfallBufCount := 0; FWfAccumFresh := False;
|
||
FWebSpecPixels := 1024; // дефолт web.spec_pixels; хост переопределит из настроек
|
||
FPAMaxPower := 100.0;
|
||
FPlutoTxMaxAttDb := -10.0; // ADI-дефолт: линейный режим выходных каскадов
|
||
FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True;
|
||
FWfPalette := 0; FWfGamma := 1.0; FDisplayPixels := 4096;
|
||
FWfSpeedDiv := 1; FSpecMSAA := 4;
|
||
FFMStepOn := True;
|
||
FRawFMDeviation := RAW_FM_DEV_DEFAULT;
|
||
FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно
|
||
TSettingsManager.DefaultCalibration(FCal);
|
||
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
|
||
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE;
|
||
FLastPlutoRSSIdB := TELEMETRY_NONE;
|
||
FLastPlutoGainDb := TELEMETRY_NONE;
|
||
FPlutoTeleTick := 0; FPlutoTempBad := 0;
|
||
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
|
||
end;
|
||
|
||
procedure TRadioController.AttachEngineCallbacks;
|
||
// Всё, что движок должен знать о САМОМ контроллере. Отдельным методом, потому
|
||
// что движок пересоздают на ходу (смена sample rate до старта): раньше эти
|
||
// колбэки навешивались только в CreateEngines, и после пересоздания движок
|
||
// оставался без отвода демодулятора — молча пропадали декодеры CW/DMR/FM RAW,
|
||
// аудио слайсов, калибровка дисплея и оба тапа TCI.
|
||
begin
|
||
if not Assigned(FDSPEngine) then Exit;
|
||
// Роутинг аудио слайсов на их звуковухи — внутренний (не зависит от
|
||
// фронтенда, в отличие от OnAudio/OnSpectrum).
|
||
FDSPEngine.OnSliceAudio := OnSliceAudioReady;
|
||
FDSPEngine.OnSliceDemodAudio := OnSliceDemodAudioReady;
|
||
FDSPEngine.OnDemodAudio := OnDemodAudioReady;
|
||
// Калибровка уровня спектра/водопада на диапазон/трансвертер — движок
|
||
// спрашивает её раз в кадр (сам он про диапазоны не знает).
|
||
FDSPEngine.OnDispCal := DispCalFor;
|
||
FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||
// Тапы наружу (TCI): их владелец живёт дольше движка.
|
||
FDSPEngine.SetIQTap(FIQTap);
|
||
// Послайсовые флаги «этот слайс слушают» сюда не входят: слайсы пересозданный
|
||
// движок не наследует вовсе (их заводит заново тот, кто их создавал), а
|
||
// владелец потоков расставит флаги на первом же обновлении карты слайсов.
|
||
// Декодер маяка QO-100 создаётся позже самого движка (в CreateEngines ниже),
|
||
// зато при ПЕРЕсоздании движка он уже есть и его надо вернуть на место.
|
||
if Assigned(FBeaconDec) then FDSPEngine.SetBeaconDecoder(FBeaconDec);
|
||
end;
|
||
|
||
procedure TRadioController.CreateEngines(ASampleRate: Integer);
|
||
// Создаёт движки в полях контроллера. Колбэки (OnAudio/OnSpectrum/OnHPStatus…)
|
||
// навешивает вызывающий после возврата — они указывают на UI/демон.
|
||
begin
|
||
FNetwork := THPSDRNetwork.Create;
|
||
|
||
// BufSize=512: латентность 512/48000 ≈ 10.7мс — допустимо для SDR; меньший
|
||
// буфер вдвое сокращает построение downsampler в OpenChannel RXA.
|
||
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
|
||
AttachEngineCallbacks;
|
||
|
||
// QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
|
||
FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink)
|
||
FBeaconLockHz := 0.0;
|
||
FBeaconLockOn := False;
|
||
|
||
// Декодер маяка: фронтенд BPSK 400 бод (тап IQ в движке).
|
||
FBeaconDec := TBeaconDecoder.Create;
|
||
// Тракт QO-100 (LNB+трансвертер) зеркалит спектр: проверено приборно (symMag ×4
|
||
// и prom 500-800 при invert=ON против шума при OFF, на наведении точно на маяк).
|
||
// Декодеру нужно сопряжение, чтобы наведённый offset доставлял маяк к DC.
|
||
FBeaconDecInvert := True;
|
||
FBcnZoomSpanHz := 40000.0;
|
||
FBcnZoomOffsetHz := 0.0;
|
||
FBcnZoomFollow := True;
|
||
FBcnZoomAuto := False;
|
||
FBeaconDec.SetInvert(FBeaconDecInvert);
|
||
FDSPEngine.SetBeaconDecoder(FBeaconDec);
|
||
|
||
// DMR front-end owns a worker thread. It stays idle until MODE_DMR is active.
|
||
FDMRDec := TDMRDecoder.Create;
|
||
// WDSP's FM discriminator polarity on the EWSDR RX path is opposite to the
|
||
// DMR dibit convention: captured handheld MS voice sync appears as MS data
|
||
// without this inversion (the two sync words are complements).
|
||
FDMRDec.SetInverted(True);
|
||
FDMRDec.OnAudio := OnDMRAudioReady;
|
||
|
||
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
||
// формы / при подключении устройства).
|
||
FAudioOut := TAudioOutput.Create(48000);
|
||
FAudioOut.OutputBufferSize := FSettings.LoadAudioBufferSize;
|
||
FAudioIn := TAudioInput.Create(48000);
|
||
end;
|
||
|
||
procedure TRadioController.FreeEngines;
|
||
var i: Integer;
|
||
begin
|
||
// Stop new slice callbacks before their decoder/audio destinations vanish.
|
||
if Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.OnSliceAudio := nil;
|
||
FDSPEngine.OnSliceDemodAudio := nil;
|
||
end;
|
||
// Stop the DMR worker before destroying any of its audio destinations.
|
||
if Assigned(FDMRDec) then
|
||
begin
|
||
FDMRDec.SetEnabled(False);
|
||
FDMRDec.OnAudio := nil;
|
||
FreeAndNil(FDMRDec);
|
||
end;
|
||
if Assigned(FNetwork) then
|
||
begin
|
||
if FNetwork.Connected then FNetwork.Disconnect;
|
||
FreeAndNil(FNetwork);
|
||
end;
|
||
// RemoveSlice takes the DSP slice lock. Besides closing the WDSP channel it
|
||
// waits out a callback that might already have entered before we detached it.
|
||
if Assigned(FDSPEngine) then
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active then FDSPEngine.RemoveSlice(FSlices[i].Id);
|
||
// Слайсы: каналы уже закрыты, останавливаем декодеры и аудио.
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active then
|
||
begin
|
||
FreeAndNil(FSlices[i].DMR);
|
||
if FSlices[i].Audio <> nil then
|
||
begin
|
||
FSlices[i].Audio.Close;
|
||
FreeAndNil(FSlices[i].Audio);
|
||
end;
|
||
FSlices[i].AudioIn := nil; // владелец — mic-пул (FreeMicPool ниже)
|
||
FSlices[i].Active := False;
|
||
end;
|
||
FreeMicPool;
|
||
if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end;
|
||
if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end;
|
||
if Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetBeaconDecoder(nil);
|
||
FDSPEngine.Close;
|
||
FreeAndNil(FDSPEngine);
|
||
end;
|
||
if Assigned(FBeaconDec) then FreeAndNil(FBeaconDec);
|
||
end;
|
||
|
||
destructor TRadioController.Destroy;
|
||
begin
|
||
// Потоки телеграфа гасим ПЕРВЫМИ: их колбэки лезут в бэкенд и в движок.
|
||
// Порядок — порт ключа → генератор: иначе лепесток успел бы разбудить уже
|
||
// разрушаемый генератор.
|
||
if Assigned(FCWKeyPort) then FreeAndNil(FCWKeyPort);
|
||
if Assigned(FCWLocal) then
|
||
begin
|
||
FCWLocal.SetArmed(False);
|
||
FreeAndNil(FCWLocal);
|
||
end;
|
||
if Assigned(FCWSender) then
|
||
begin
|
||
FCWSender.AbortSending;
|
||
FreeAndNil(FCWSender);
|
||
end;
|
||
if Assigned(FCWElem) then
|
||
begin
|
||
FCWElem.AbortPlay;
|
||
FreeAndNil(FCWElem);
|
||
end;
|
||
if Assigned(FCWDec) then FCWDec.Enabled := False; // кольцо больше не кормим
|
||
FreeEngines;
|
||
// Декодер — ПОСЛЕ движка: его кольцо наполняет DSP-поток, и освобождать
|
||
// объект, пока поток жив, нельзя.
|
||
if Assigned(FCWDec) then FreeAndNil(FCWDec);
|
||
FreeAndNil(FDeviceStore);
|
||
FreeAndNil(FSettings);
|
||
// Лок тапов — последним: до FreeEngines по нему ходит DSP-поток.
|
||
FreeAndNil(FAudioTapLock);
|
||
inherited Destroy;
|
||
end;
|
||
|
||
procedure TRadioController.Changed(Field: TRadioField);
|
||
var i: Integer;
|
||
begin
|
||
if Assigned(FOnStateChanged) then FOnStateChanged(Self, Field);
|
||
for i := 0 to High(FStateListeners) do
|
||
if Assigned(FStateListeners[i]) then FStateListeners[i](Self, Field);
|
||
end;
|
||
|
||
procedure TRadioController.AddStateListener(L: TRadioStateEvent);
|
||
begin
|
||
if not Assigned(L) then Exit;
|
||
SetLength(FStateListeners, Length(FStateListeners) + 1);
|
||
FStateListeners[High(FStateListeners)] := L;
|
||
end;
|
||
|
||
procedure TRadioController.AddAudioTap(T: TRadioAudioTapEvent);
|
||
begin
|
||
if not Assigned(T) then Exit;
|
||
FAudioTapLock.Enter;
|
||
try
|
||
SetLength(FAudioTaps, Length(FAudioTaps) + 1);
|
||
FAudioTaps[High(FAudioTaps)] := T;
|
||
FAudioTapCount := Length(FAudioTaps);
|
||
finally
|
||
FAudioTapLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.RemoveAudioTap(T: TRadioAudioTapEvent);
|
||
var i, j: Integer;
|
||
begin
|
||
if not Assigned(T) then Exit;
|
||
FAudioTapLock.Enter;
|
||
try
|
||
for i := 0 to High(FAudioTaps) do
|
||
if (TMethod(FAudioTaps[i]).Code = TMethod(T).Code) and
|
||
(TMethod(FAudioTaps[i]).Data = TMethod(T).Data) then
|
||
begin
|
||
for j := i to High(FAudioTaps) - 1 do FAudioTaps[j] := FAudioTaps[j + 1];
|
||
SetLength(FAudioTaps, Length(FAudioTaps) - 1);
|
||
FAudioTapCount := Length(FAudioTaps);
|
||
Break;
|
||
end;
|
||
finally
|
||
FAudioTapLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.FireAudioTap(Kind: TRadioAudioKind;
|
||
PanId, SliceId: Integer; const Left, Right: array of Single; Count: Integer);
|
||
// DSP-поток. Без единого тапа (обычный случай) — одна проверка целого и выход:
|
||
// лок в звуковом маршруте на каждый блок иначе стоил бы дороже самой работы.
|
||
var i: Integer;
|
||
begin
|
||
if FAudioTapCount = 0 then Exit;
|
||
FAudioTapLock.Enter;
|
||
try
|
||
for i := 0 to High(FAudioTaps) do
|
||
if Assigned(FAudioTaps[i]) then
|
||
FAudioTaps[i](Kind, PanId, SliceId, Left, Right, Count);
|
||
finally
|
||
FAudioTapLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.SetIQTap(T: TIQTapEvent);
|
||
// Тап помним у себя: движок пересоздают (RecreateDSPEngine при смене sample
|
||
// rate до старта), и новый про тап не знал бы — поток IQ по TCI молча умирал
|
||
// на всю сессию. Восстанавливает его AttachEngineCallbacks.
|
||
begin
|
||
FIQTap := T;
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetIQTap(T);
|
||
end;
|
||
|
||
function TRadioController.IQTapRateHz(PanId: Integer): Integer;
|
||
begin
|
||
if PanId <= 0 then Result := FSampleRate
|
||
else Result := PanDDCRateKHz(PanId) * 1000;
|
||
end;
|
||
|
||
procedure TRadioController.PushTCIAudio(const Samples: array of Double; N: Integer);
|
||
// Поток клиента TCI. Ринг микрофона в движке лок-фри и рассчитан ровно на это
|
||
// (тем же путём ходит web-микрофон); переполнение — дроп, как у железа.
|
||
begin
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.PushTXMicSamplesD(Samples, N);
|
||
end;
|
||
|
||
procedure TRadioController.RemoveStateListener(L: TRadioStateEvent);
|
||
var i, j: Integer;
|
||
begin
|
||
if not Assigned(L) then Exit;
|
||
for i := 0 to High(FStateListeners) do
|
||
if (TMethod(FStateListeners[i]).Code = TMethod(L).Code) and
|
||
(TMethod(FStateListeners[i]).Data = TMethod(L).Data) then
|
||
begin
|
||
for j := i to High(FStateListeners) - 1 do
|
||
FStateListeners[j] := FStateListeners[j + 1];
|
||
SetLength(FStateListeners, Length(FStateListeners) - 1);
|
||
Exit;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.PushNetworkState;
|
||
// Полный HP-кадр в радио: RX/TX частоты (через трансвертор), drive, состояние
|
||
// MOX. Вызывается командами, которые меняют центр/частоту/репитер-сдвиг.
|
||
begin
|
||
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
// Состояние передачи для ЖЕЛЕЗА — RadioKeyed, а не FTransmitting: в
|
||
// телеграфе ключ замыкает прошивка, и реле T/R с антенной и OC обязаны
|
||
// ехать за ней. PTT в кадре этим не поднимается (он живёт в SendPTT).
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz,
|
||
FDriveLevel, RadioKeyed, True, True, FTuning, FTwoTone);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket);
|
||
// Сетевой поток: трекинг seq + подача IQ активного DDC в WDSP. UI не трогаем —
|
||
// статус рисует таймер по полям ниже. Backend-агностичен (Pluto синтезирует
|
||
// тот же TDDCIQPacket).
|
||
var
|
||
SamplesPerFrame: Integer;
|
||
Seq, ExpectedSeq: LongWord;
|
||
Delta: Int64;
|
||
begin
|
||
Inc(FRXPacketCount);
|
||
FRXLastPktTime := GetTickCount64;
|
||
Seq := (LongWord(Data.Seq[0]) shl 24) or (LongWord(Data.Seq[1]) shl 16)
|
||
or (LongWord(Data.Seq[2]) shl 8) or LongWord(Data.Seq[3]);
|
||
|
||
if (DDCIndex >= Low(FDDCLastSeq)) and (DDCIndex <= High(FDDCLastSeq)) then
|
||
begin
|
||
if Seq <> 0 then
|
||
begin
|
||
ExpectedSeq := FDDCLastSeq[DDCIndex] + 1;
|
||
if FDDCSeqValid[DDCIndex] and (Seq <> ExpectedSeq) then
|
||
begin
|
||
Inc(FSeqErrorCount);
|
||
FLastSeqErrorDDC := DDCIndex;
|
||
Delta := Int64(Seq) - Int64(ExpectedSeq);
|
||
FLastSeqErrorDelta := Delta;
|
||
FSeqOkStreak := 0;
|
||
end
|
||
else if FDDCSeqValid[DDCIndex] then
|
||
begin
|
||
if FSeqErrorCount > 0 then
|
||
begin
|
||
Inc(FSeqOkStreak);
|
||
if FSeqOkStreak >= 20 then
|
||
begin
|
||
FSeqErrorCount := 0;
|
||
FLastSeqErrorDDC := -1;
|
||
FLastSeqErrorDelta := 0;
|
||
FSeqOkStreak := 0;
|
||
end;
|
||
end;
|
||
end;
|
||
end;
|
||
FDDCLastSeq[DDCIndex] := Seq;
|
||
FDDCSeqValid[DDCIndex] := True;
|
||
end;
|
||
|
||
// Подаём IQ в DSP: активный DDC → главный тракт, пан-DDC → его пан (3.2),
|
||
// DDC0 при вооружённом PureSignal на TX → feedback в pscc (DDC0 несёт
|
||
// interleaved пары DDC0+DDC1; на PS-платах главный DDC ≥ 2, конфликта нет).
|
||
if FWDSPReady then
|
||
begin
|
||
SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or
|
||
Integer(Data.SamplesPerFrame[1]);
|
||
if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback
|
||
if (DDCIndex = 0) and FPSFeedbackOn and (FActiveDDC <> 0) then
|
||
begin
|
||
if FTransmitting then
|
||
FDSPEngine.PushPSFeedback(Data.IQData, 0, SamplesPerFrame);
|
||
end
|
||
else if DDCIndex = FActiveDDC then
|
||
FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame)
|
||
else if (DDCIndex >= 0) and (DDCIndex < MAX_DDCS)
|
||
and (FDDCPanMap[DDCIndex] > 0) then
|
||
FDSPEngine.PushPanIQ(FDDCPanMap[DDCIndex], Data.IQData, 0, SamplesPerFrame);
|
||
end;
|
||
|
||
// Запоминаем последний seq — статус обновит таймер (без Synchronize в hot path)
|
||
FLastDDCSeq := Seq;
|
||
FLastDDCIndex := DDCIndex;
|
||
end;
|
||
|
||
procedure TRadioController.OnTXIQ(const Buf: array of Double; Count: Integer);
|
||
// WDSP TXA отдаёт IQ-пары @ TX rate. Копим хвост между вызовами и шлём только
|
||
// полные DUC-пакеты по 240 (зануление хвоста давало 47 Гц гул). Клампим в
|
||
// 24-bit, чтобы пики компрессора/EQ (чуть >1.0) не оборачивались через wrap.
|
||
// Backend-агностично: Pluto-backend в SendDUCIQ переведёт 24-bit→int16.
|
||
function ScaleIQ24(V: Double): Integer;
|
||
var S: Double;
|
||
begin
|
||
S := V * 8388607.0;
|
||
if S > 8388607.0 then Result := 8388607
|
||
else if S < -8388608.0 then Result := -8388608
|
||
else Result := Round(S);
|
||
end;
|
||
var
|
||
i: Integer;
|
||
begin
|
||
if not (Assigned(FNetwork) and FNetwork.Connected) then Exit;
|
||
for i := 0 to Count - 1 do
|
||
begin
|
||
FDUCPendingI[FDUCPendingCount] := ScaleIQ24(Buf[i * 2]);
|
||
FDUCPendingQ[FDUCPendingCount] := ScaleIQ24(Buf[i * 2 + 1]);
|
||
Inc(FDUCPendingCount);
|
||
if FDUCPendingCount >= 240 then
|
||
begin
|
||
FNetwork.SendDUCIQ(FDUCPendingI, FDUCPendingQ);
|
||
FDUCPendingCount := 0;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.OnHPStatus(const Status: THighPriorityStatus);
|
||
// Сетевой поток: декод HP-статуса (board-specific supply/power/SWR) → pending →
|
||
// маршалинг в ApplyHPStatus (сглаживание + overload + HWPTT уже на потоке UI/ядра).
|
||
const
|
||
SWR_MIN_FWD_W = 1.0; // ниже — fwd это ADC-шум, SWR не считаем
|
||
SWR_CAP = 9.9;
|
||
BAND_6M = 10; // индекс 6m в HF-плане (160m..6m)
|
||
var
|
||
FwdPwr, RevPwr: Word;
|
||
SupplyV, SupplyA, FwdW, RevW, SWRV, Rho, DetK: Double;
|
||
Bi: Integer;
|
||
Is6m, IsTx: Boolean;
|
||
KeyDot, KeyDash: Boolean;
|
||
begin
|
||
FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo;
|
||
RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo;
|
||
SupplyV := -1.0;
|
||
SupplyA := -1.0;
|
||
|
||
// Board 5 (Orion MkII): напряжение на UserADC0, ток на UserADC1 (как Thetis).
|
||
// Прочие платы — в SupplyVolts-байтах, без тока.
|
||
if BoardSupportsSupplyVoltage(FNetwork.Device.BoardType) then
|
||
begin
|
||
if FNetwork.Device.BoardType = 5 then
|
||
SupplyV := ADCToSupplyVolts(
|
||
(Status.UserADC0Hi shl 8) or Status.UserADC0Lo,
|
||
FNetwork.Device.BoardType)
|
||
else
|
||
SupplyV := ADCToSupplyVolts(
|
||
(Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo,
|
||
FNetwork.Device.BoardType);
|
||
if SupplyV >= 0 then SupplyV := SupplyV * FCal.SupplyVoltCal;
|
||
|
||
if BoardSupportsSupplyCurrent(FNetwork.Device.BoardType) then
|
||
SupplyA := ADCToSupplyCurrent(
|
||
(Status.UserADC1Hi shl 8) or Status.UserADC1Lo,
|
||
FNetwork.Device.BoardType,
|
||
FCal.AmpVoffMV, FCal.AmpSensMVA);
|
||
end;
|
||
// Детектор Alex-моста: константы по модели (вкладка Calibration) + per-band
|
||
// множитель. Множитель общий для FWD/REV, поэтому КСВ от него не зависит
|
||
// (как Thetis PABandCalMult).
|
||
Bi := TxBandIdx;
|
||
Is6m := Bi = BAND_6M;
|
||
DetK := 1.0;
|
||
if (Bi >= 0) and (Bi < CFG_BAND_COUNT) and (FCal.PADetCal[Bi] > 0) then
|
||
DetK := FCal.PADetCal[Bi];
|
||
FwdW := AlexDetToWatts(FwdPwr, FCal.PwrDetModel, False, Is6m) * DetK;
|
||
RevW := AlexDetToWatts(RevPwr, FCal.PwrDetModel, True, Is6m) * DetK;
|
||
// FTransmitting — программный флаг; HPS_PTT — вход HW-PTT (footswitch/mic).
|
||
IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0);
|
||
// SWR = (1+ρ)/(1-ρ), ρ = sqrt(RevW/FwdW) (Thetis); только при значимой
|
||
// fwd-мощности.
|
||
if IsTx and (FwdW >= SWR_MIN_FWD_W) then
|
||
begin
|
||
Rho := Sqrt(RevW / FwdW);
|
||
if Rho >= 0.99 then
|
||
SWRV := SWR_CAP
|
||
else
|
||
begin
|
||
SWRV := (1.0 + Rho) / (1.0 - Rho);
|
||
if SWRV > SWR_CAP then SWRV := SWR_CAP;
|
||
if SWRV < 1.0 then SWRV := 1.0;
|
||
end;
|
||
end else
|
||
SWRV := 1.0;
|
||
if not IsTx then FwdW := 0;
|
||
|
||
FHPpFwdW := FwdW;
|
||
FHPpSWR := SWRV;
|
||
FHPpSupplyV := SupplyV;
|
||
FHPpSupplyA := SupplyA;
|
||
FHPpPLL := (Status.StatusBits and HPS_PLL_LOCKED) <> 0;
|
||
FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0;
|
||
FHPpOverload := Status.ADCOverload;
|
||
// Состояние ключа/манипулятора в разъёме трансивера (сетевой поток).
|
||
// Касание обрывает программную передачу — как в Thetis/pihpsdr («key hit»
|
||
// останавливает CAT CW), иначе оператор не может вклиниться. ★Обрываем по
|
||
// ФРОНТУ, а не по уровню: на плате без подключённого ключа входы могут стоять
|
||
// в единице, и проверка уровня убивала бы передачу на первом же HP-статусе.
|
||
KeyDot := (Status.StatusBits and HPS_DOT) <> 0;
|
||
KeyDash := (Status.StatusBits and HPS_DASH) <> 0;
|
||
if ((KeyDot and not FCWPrevDot) or (KeyDash and not FCWPrevDash))
|
||
and CWXBusy then CWXAbort;
|
||
FCWPrevDot := KeyDot;
|
||
FCWPrevDash := KeyDash;
|
||
// Программный сайдтон от лепестков верен ТОЛЬКО у прямого ключа: при иамбике
|
||
// элементы считает FPGA, а PC видит лишь замыкание контакта.
|
||
if (FCWSettings.KeyerMode = 0) and (ActiveTXMode in [MODE_CWL, MODE_CWU])
|
||
and (not CWXBusy) then
|
||
FCWKeyDown := KeyDot or KeyDash;
|
||
Invoke(ApplyHPStatus);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyHPStatus;
|
||
// Маршалится из OnHPStatus. PEP-style баллистика (мгновенная атака, медленный
|
||
// спад) для FwdW/SWR; EMA для supply. + overload-событие + HWPTT-фронт → SetMOX.
|
||
const
|
||
DECAY_FWD = 0.02;
|
||
DECAY_SWR = 0.015;
|
||
ALPHA_SUPPLY = 0.05;
|
||
begin
|
||
if FHPpFwdW <= 0 then
|
||
FLastFwdW := 0
|
||
else if FHPpFwdW > FLastFwdW then
|
||
FLastFwdW := FHPpFwdW // attack: instant
|
||
else
|
||
FLastFwdW := DECAY_FWD * FHPpFwdW + (1.0 - DECAY_FWD) * FLastFwdW;
|
||
if FHPpSWR <= 1.0 then
|
||
FLastSWR := 1.0
|
||
else if FHPpSWR > FLastSWR then
|
||
FLastSWR := FHPpSWR // attack: instant
|
||
else
|
||
FLastSWR := DECAY_SWR * FHPpSWR + (1.0 - DECAY_SWR) * FLastSWR;
|
||
if FHPpSupplyV >= 0 then
|
||
begin
|
||
if FLastSupplyV < 0 then
|
||
FLastSupplyV := FHPpSupplyV // первое значение — без сглаживания
|
||
else
|
||
FLastSupplyV := ALPHA_SUPPLY * FHPpSupplyV + (1.0 - ALPHA_SUPPLY) * FLastSupplyV;
|
||
end else
|
||
FLastSupplyV := -1.0;
|
||
if FHPpSupplyA >= 0 then
|
||
begin
|
||
if FLastSupplyA < 0 then
|
||
FLastSupplyA := FHPpSupplyA
|
||
else
|
||
FLastSupplyA := ALPHA_SUPPLY * FHPpSupplyA + (1.0 - ALPHA_SUPPLY) * FLastSupplyA;
|
||
end else
|
||
FLastSupplyA := -1.0;
|
||
FLastPLLLock := FHPpPLL;
|
||
|
||
// ADC overload — индикатор рисует UI по rfADCOverload (только на изменении).
|
||
if FADCOverloadActive <> (FHPpOverload <> 0) then
|
||
begin
|
||
FADCOverloadActive := FHPpOverload <> 0;
|
||
Changed(rfADCOverload);
|
||
end;
|
||
|
||
// Вооружение кейера — под текущий контекст (бэнд/трансвертер/Alex могли
|
||
// поменяться мимо SetMode и SetTxSlice). Вызов идемпотентный, см. SyncCWKeyer.
|
||
SyncCWKeyer;
|
||
// ★Телеграф с кейером в прошивке: HPS_PTT здесь — ЭХО собственной манипуляции
|
||
// (при break-in железо само поднимает T/R на каждую посылку), а не действие
|
||
// оператора. Гонять по нему SetMOX нельзя: на каждой точке пересобирался бы
|
||
// передающий тракт, а программная передача обрывалась бы после ПЕРВОЙ посылки
|
||
// (снятие MOX гасит CWX). Флаг держим актуальным — им выбирается источник
|
||
// модуляции, — но передачей по нему не управляем.
|
||
if CWTXActive then
|
||
begin
|
||
// ★Ниже стоит Exit, поэтому падающий фронт HWPTT здесь обработать НЕКОМУ.
|
||
// Если в эфир нас увёл именно HWPTT, пока источником передачи был не
|
||
// телеграф (например слайс в голосовом режиме), а телеграф включился уже
|
||
// после — передача залипала бы НАВСЕГДА: FTransmitting остаётся взведённым,
|
||
// Alex стоит в TX, PTT не снимается, и приёмник всю дорогу видит свой же
|
||
// передатчик. Снимаем такую передачу здесь.
|
||
if FHWPTTStartedTX and (not FHPpHWPTT) then
|
||
begin
|
||
FHWPTTStartedTX := False;
|
||
SetMOX(False);
|
||
end;
|
||
FHWPTTActive := FHPpHWPTT;
|
||
if FHPpHWPTT then FCWKeyedAt := GetTickCount64;
|
||
ServiceCWKeyed;
|
||
Exit;
|
||
end;
|
||
ServiceCWKeyed; // ушли из телеграфа — вернуть индикацию в исходное
|
||
// Hardware PTT (footswitch / mic PTT): фронт на rate HP Status — не пропустить
|
||
// короткое нажатие. HWPTT не перекрывает ручной MOX; выключаем TX только если
|
||
// его включил именно HWPTT.
|
||
if FHPpHWPTT <> FHWPTTActive then
|
||
begin
|
||
FHWPTTActive := FHPpHWPTT;
|
||
if not FTuning then
|
||
begin
|
||
if FHPpHWPTT then
|
||
begin
|
||
if not FTransmitting then
|
||
begin
|
||
SetMOX(True);
|
||
FHWPTTStartedTX := True;
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
if FHWPTTStartedTX then
|
||
begin
|
||
FHWPTTStartedTX := False;
|
||
SetMOX(False);
|
||
end;
|
||
end;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.OnAudioReady(const Left, Right: array of Single;
|
||
Count: Integer);
|
||
// DSP-поток: маршрут RX-аудио. Встроенный динамик трансивера (DDC Audio,
|
||
// port 1028) — независимый канал. Затем web-адаптер может перехватить аудио
|
||
// (активный клиент); если перехвачено — в локальный AudioOut не пишем.
|
||
begin
|
||
// Decoded DMR PCM will use this route later. Until then suppress raw 4FSK;
|
||
// the independent pre-volume tap is handled by OnDemodAudioReady.
|
||
if FMode in [MODE_DMR, MODE_FMRAW] then Exit;
|
||
|
||
// Программный сайдтон подмешиваем ДО всех потребителей: его должны слышать
|
||
// и локальная звуковуха, и web-клиент, и динамик трансивера.
|
||
if MixSidetone(Left, Right, Count) then
|
||
begin
|
||
if FSendAudioToRadio then
|
||
FNetwork.SendSpeakerAudio(FCWSTBufL, FCWSTBufR, Count);
|
||
// Линейный выход (TCI LINEOUT_STREAM) — здесь: это ровно то, что слышно,
|
||
// вместе с сайдтоном и после громкости. Тап слушает, ничего не забирая,
|
||
// поэтому стоит ДО перехвата web-клиентом.
|
||
FireAudioTap(rakLineOut, 0, 0, FCWSTBufL, FCWSTBufR, Count);
|
||
if Assigned(FOnAudioConsume) and FOnAudioConsume(FCWSTBufL, FCWSTBufR, Count) then
|
||
Exit;
|
||
if FLocalAudio then
|
||
FAudioOut.Write(FCWSTBufL, FCWSTBufR, Count);
|
||
Exit;
|
||
end;
|
||
|
||
if FSendAudioToRadio then
|
||
FNetwork.SendSpeakerAudio(Left, Right, Count);
|
||
FireAudioTap(rakLineOut, 0, 0, Left, Right, Count);
|
||
if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, Count) then
|
||
Exit;
|
||
// Без локального звука (демон) на этом маршрут заканчивается: если web-клиент
|
||
// не активен, аудио просто отбрасывается, а не идёт на звуковую карту хоста.
|
||
if FLocalAudio then
|
||
FAudioOut.Write(Left, Right, Count);
|
||
end;
|
||
|
||
procedure TRadioController.OnDemodAudioReady(const Left, Right: array of Single;
|
||
Count: Integer);
|
||
// DSP thread, pre-volume/pre-mute. FeedAudio only copies into a bounded ring.
|
||
begin
|
||
// Аудиопоток приёмника для TCI (§3.4) — именно отсюда: скиммеру и цифре
|
||
// нужен звук приёмника, а не то, что осталось после ручки громкости и
|
||
// мьюта. Линейный выход у TCI отдельным потоком (см. OnAudioReady).
|
||
FireAudioTap(rakDemod, 0, 0, Left, Right, Count);
|
||
if FMode = MODE_DMR then
|
||
begin
|
||
if not Assigned(FDMRDec) then Exit;
|
||
if not FDMRDec.Enabled then FDMRDec.SetEnabled(True);
|
||
FDMRDec.FeedAudio(Left, Right, Count);
|
||
end
|
||
else if FMode = MODE_FMRAW then
|
||
begin
|
||
// FM RAW decoder audio deliberately bypasses speaker volume and mute.
|
||
// Значит и линейный выход у него — вот этот сигнал: OnAudioReady для
|
||
// FM RAW выходит в самом начале, и без этой строки поток LINEOUT по TCI
|
||
// на FM RAW не шёл вовсе.
|
||
FireAudioTap(rakLineOut, 0, 0, Left, Right, Count);
|
||
if FLocalAudio and Assigned(FAudioOut) then
|
||
FAudioOut.Write(Left, Right, Count);
|
||
end
|
||
else if Assigned(FDMRDec) and FDMRDec.Enabled then
|
||
FDMRDec.SetEnabled(False);
|
||
// ★Телеграфный декодер кормим отсюда же: здесь аудио идёт ДО громкости и
|
||
// мьюта и БЕЗ программного сайдтона (он подмешивается позже, в OnAudioReady),
|
||
// зато уже после узкого CW-фильтра — лучшего предетектора не найти.
|
||
// На своей передаче молчим: в телеграфе приёмник намеренно жив, и собственный
|
||
// сигнал (особенно в полном дуплексе Pluto) забил бы ленту своей же работой.
|
||
if Assigned(FCWDec) and FCWDec.Enabled
|
||
and (FMode in [MODE_CWL, MODE_CWU]) and (not RadioKeyed) then
|
||
FCWDec.FeedAudio(Left, Count);
|
||
end;
|
||
|
||
procedure TRadioController.OnDMRAudioReady(const PCM: array of Single;
|
||
Count: Integer);
|
||
// DMR worker thread, 8 kHz mono from mbelib. Linear 6x interpolation brings
|
||
// it to the common 48 kHz output route used by local, web and radio speakers.
|
||
var
|
||
Left, Right: array of Single;
|
||
i, Phase, OutPos, N: Integer;
|
||
A, B, V, Gain: Single;
|
||
begin
|
||
if (FMode <> MODE_DMR) or FMuted or (Count <= 0) then Exit;
|
||
N := Min(Count, Length(PCM));
|
||
SetLength(Left, N * 6);
|
||
SetLength(Right, N * 6);
|
||
Gain := FVolume / 100.0;
|
||
OutPos := 0;
|
||
for i := 0 to N - 1 do
|
||
begin
|
||
A := PCM[i];
|
||
if i + 1 < N then B := PCM[i + 1] else B := A;
|
||
for Phase := 0 to 5 do
|
||
begin
|
||
V := (A + (B - A) * (Phase / 6.0)) * Gain;
|
||
Left[OutPos] := V;
|
||
Right[OutPos] := V;
|
||
Inc(OutPos);
|
||
end;
|
||
end;
|
||
if FSendAudioToRadio and Assigned(FNetwork) then
|
||
FNetwork.SendSpeakerAudio(Left, Right, OutPos);
|
||
FireAudioTap(rakLineOut, 0, 0, Left, Right, OutPos);
|
||
if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, OutPos) then Exit;
|
||
if FLocalAudio and Assigned(FAudioOut) then FAudioOut.Write(Left, Right, OutPos);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Мультислайсы (софт-fan-out)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function TRadioController.FindSliceIndex(Id: Integer): Integer;
|
||
var i: Integer;
|
||
begin
|
||
Result := -1;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].Id = Id) then Exit(i);
|
||
end;
|
||
|
||
// DSP-поток: аудио готового слайса → его собственный выход. Слайс — локальный
|
||
// монитор (в web/на радио не уходит), поэтому только на свою звуковуху.
|
||
procedure TRadioController.OnSliceAudioReady(SliceId: Integer;
|
||
const Left, Right: array of Single; Count: Integer);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(SliceId);
|
||
if idx < 0 then Exit;
|
||
// Линейный выход слайса для TCI — до гейта локального звука: в демоне
|
||
// звуковухи нет вовсе, а поток клиенту идти обязан.
|
||
FireAudioTap(rakLineOut, FSlices[idx].PanId, SliceId, Left, Right, Count);
|
||
if not FLocalAudio then Exit;
|
||
if FSlices[idx].Mode in [MODE_DMR, MODE_FMRAW] then Exit;
|
||
if SliceMutedByTx(idx) then Exit;
|
||
if FSlices[idx].Audio <> nil then
|
||
FSlices[idx].Audio.Write(Left, Right, Count);
|
||
end;
|
||
|
||
// Слайс глушится на передаче? В full-duplex (Pluto/QO-100) RX-тракт живёт и во
|
||
// время TX, поэтому иначе оператор слышит сам себя на КАЖДОМ слайсе. У главного
|
||
// приёмника за это отвечает FRxMuteOnTx (кнопка RX MUTE), у слайса — свой флаг.
|
||
function TRadioController.SliceMutedByTx(Idx: Integer): Boolean;
|
||
begin
|
||
Result := FTransmitting and FSlices[Idx].RxMuteOnTx;
|
||
end;
|
||
|
||
// DSP thread: flat, volume-independent discriminator of the corresponding
|
||
// WDSP slice channel. FeedAudio only copies it into that decoder's ring.
|
||
procedure TRadioController.OnSliceDemodAudioReady(SliceId: Integer;
|
||
const Left, Right: array of Single; Count: Integer);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(SliceId);
|
||
if idx < 0 then Exit;
|
||
FireAudioTap(rakDemod, FSlices[idx].PanId, SliceId, Left, Right, Count);
|
||
if (FSlices[idx].Mode = MODE_DMR) and Assigned(FSlices[idx].DMR) then
|
||
FSlices[idx].DMR.FeedAudio(Left, Right, Count);
|
||
if FSlices[idx].Mode = MODE_FMRAW then
|
||
begin
|
||
// У FM RAW звук идёт мимо громкости и мьюта, поэтому линейный выход слайса
|
||
// — это он же (OnSliceAudioReady для FM RAW не вызывается вовсе). Без этой
|
||
// строки поток LINEOUT по TCI на FM RAW-слайсе молчал.
|
||
FireAudioTap(rakLineOut, FSlices[idx].PanId, SliceId, Left, Right, Count);
|
||
if FLocalAudio and Assigned(FSlices[idx].Audio) then
|
||
FSlices[idx].Audio.Write(Left, Right, Count);
|
||
end;
|
||
end;
|
||
|
||
// DMR worker thread: decoded 8 kHz mono -> this slice's 48 kHz local output.
|
||
procedure TRadioController.OnSliceDMRAudioReady(SliceId: Integer;
|
||
const PCM: array of Single; Count: Integer);
|
||
var
|
||
Left, Right: array of Single;
|
||
idx, i, Phase, OutPos, N: Integer;
|
||
A, B, V, Gain: Single;
|
||
begin
|
||
if (Count <= 0) or not (FLocalAudio or (FAudioTapCount > 0)) then Exit;
|
||
idx := FindSliceIndex(SliceId);
|
||
if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) or FSlices[idx].Muted then Exit;
|
||
if SliceMutedByTx(idx) then Exit;
|
||
// Звуковухи может не быть вовсе (демон), но линейный выход слайса слушает
|
||
// ещё и TCI — тогда декодированный звук всё равно надо построить.
|
||
if not (Assigned(FSlices[idx].Audio) or (FAudioTapCount > 0)) then Exit;
|
||
N := Min(Count, Length(PCM));
|
||
SetLength(Left, N * 6);
|
||
SetLength(Right, N * 6);
|
||
Gain := FSlices[idx].Volume;
|
||
OutPos := 0;
|
||
for i := 0 to N - 1 do
|
||
begin
|
||
A := PCM[i];
|
||
if i + 1 < N then B := PCM[i + 1] else B := A;
|
||
for Phase := 0 to 5 do
|
||
begin
|
||
V := (A + (B - A) * (Phase / 6.0)) * Gain;
|
||
Left[OutPos] := V;
|
||
Right[OutPos] := V;
|
||
Inc(OutPos);
|
||
end;
|
||
end;
|
||
// Линейный выход DMR-слайса — декодированный звук, как и у главного тракта
|
||
// (см. OnDMRAudioReady): 4FSK до декодера в LINEOUT слать нечего.
|
||
FireAudioTap(rakLineOut, FSlices[idx].PanId, SliceId, Left, Right, OutPos);
|
||
if FLocalAudio and Assigned(FSlices[idx].Audio) then
|
||
FSlices[idx].Audio.Write(Left, Right, OutPos);
|
||
end;
|
||
|
||
procedure TRadioController.RefreshSliceShifts;
|
||
// Смена центра ГЛАВНОГО захвата: пересчитываем только слайсы пана 0 —
|
||
// слайсы доп. панов привязаны к центрам своих DDC (SetPanDDCFreq).
|
||
var i: Integer;
|
||
begin
|
||
if not Assigned(FDSPEngine) then Exit;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = 0) then
|
||
FDSPEngine.SetSliceShift(FSlices[i].Id, FSlices[i].TargetHz - FCenterFreq);
|
||
end;
|
||
|
||
function TRadioController.PanCenterHz(PanId: Integer): Double;
|
||
begin
|
||
if (PanId >= 1) and (PanId < MAX_PANS) and (FPanDDCIdx[PanId] >= 0) then
|
||
Result := FPanFreqHz[PanId]
|
||
else
|
||
Result := FCenterFreq;
|
||
end;
|
||
|
||
procedure TRadioController.CapturedSpan(out LoHz, HiHz: Double; PanId: Integer);
|
||
// Полезная полоса ±47.5% от захвата (запас на краях фильтра/анти-алиасинга).
|
||
var Half, Rate: Double;
|
||
begin
|
||
if (PanId >= 1) and (PanId < MAX_PANS) and (FPanDDCIdx[PanId] >= 0) then
|
||
Rate := FPanRateKHz[PanId] * 1000.0
|
||
else
|
||
Rate := FSampleRate;
|
||
Half := Rate * 0.475;
|
||
LoHz := PanCenterHz(PanId) - Half;
|
||
HiHz := PanCenterHz(PanId) + Half;
|
||
end;
|
||
|
||
function TRadioController.SliceFitsCapture(TargetHz: Double; PanId: Integer): Boolean;
|
||
var Lo, Hi: Double;
|
||
begin
|
||
CapturedSpan(Lo, Hi, PanId);
|
||
Result := (TargetHz >= Lo) and (TargetHz <= Hi);
|
||
end;
|
||
|
||
function TRadioController.CurrentBandEdges(out LoHz, HiHz: Double): Boolean;
|
||
begin
|
||
LoHz := 0; HiHz := 0;
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
|
||
begin
|
||
// Трансвертер: диапазон — его собственный (2 м = 144..146 и т.п.).
|
||
LoHz := FXvtrSettings.Entries[FCurrentXvtr].FreqBegin;
|
||
HiHz := FXvtrSettings.Entries[FCurrentXvtr].FreqEnd;
|
||
Result := HiHz > LoHz;
|
||
end
|
||
else
|
||
Result := BandEdges(FCurrentBand, IsPluto, LoHz, HiHz);
|
||
end;
|
||
|
||
procedure TRadioController.SliceFreqChanged(Id: Integer);
|
||
// Слайс перестроен НЕ из UI (CAT-порт слайса). Позицию флага UI держит сам
|
||
// (TickFlags → LayoutFlags читает живой TargetHz), а вот ЧАСТОТА и кромки
|
||
// фильтра во флаге живут в его собственных полях и обновляются только
|
||
// PushSliceFlagState. Без этого уведомления флаг уезжал на новую частоту,
|
||
// показывая старые цифры и старую полосу фильтра.
|
||
begin
|
||
FSliceFreqId := Id;
|
||
Changed(rfSliceFreq);
|
||
end;
|
||
|
||
procedure TRadioController.SliceStateChanged(Id: Integer);
|
||
// У слайса сменилось что-то кроме частоты: режим, фильтр, АРУ, DSP, громкость,
|
||
// мьют, шумоподавитель. Зовут сами сеттеры — слайс правят и UI, и CAT, и TCI, а
|
||
// узнать об этом должны все фронтенды сразу (у главного тракта для этого есть
|
||
// свои поля rfMode/rfFilter/…, у слайсов не было ничего).
|
||
begin
|
||
FSliceFreqId := Id;
|
||
Changed(rfSliceState);
|
||
end;
|
||
|
||
function TRadioController.TuneSliceInBand(Id: Integer; TargetHz: Double): Boolean;
|
||
// Свобода внешней программы: она двигает слайс куда угодно ВНУТРИ включённого
|
||
// диапазона (2 м на трансвертере: 144.174 при окне на 145.5 — можно), но сменить
|
||
// диапазон (7 МГц) — нет: это решение оператора, а не управляющего ПО.
|
||
//
|
||
// Если цель вне захваченной полосы — окно DDC переезжает центром на цель:
|
||
// • доп. пан (свой DDC) — свободно;
|
||
// • пан 0 — только если главный приёмник останется внутри нового окна
|
||
// (одно железное окно на двоих; иначе главный оглох бы).
|
||
var
|
||
idx, PanId: Integer;
|
||
Lo, Hi, Half, Dist, NewCenter, VisLo, VisHi: Double;
|
||
begin
|
||
Result := False;
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
if not CurrentBandEdges(Lo, Hi) then Exit;
|
||
if (TargetHz < Lo) or (TargetHz > Hi) then Exit; // другой диапазон — отказ
|
||
|
||
PanId := FSlices[idx].PanId;
|
||
if SliceFitsCapture(TargetHz, PanId) then
|
||
begin
|
||
SetSliceTarget(Id, TargetHz); // он же шлёт SliceFreqChanged
|
||
Exit(True);
|
||
end;
|
||
|
||
if PanId > 0 then
|
||
begin
|
||
if not PanDDCActive(PanId) then Exit;
|
||
VisibleFreqBounds(VisLo, VisHi);
|
||
NewCenter := EnsureRange(TargetHz, VisLo, VisHi);
|
||
SetPanDDCFreq(PanId, NewCenter);
|
||
SetSliceTarget(Id, TargetHz);
|
||
Changed(rfPanFreq); // UI: шапка/зум-бар/флаги пана под новый центр
|
||
// (несёт и перезалив флагов — SliceFreqChanged лишний)
|
||
Result := True;
|
||
end
|
||
else
|
||
begin
|
||
// Пан 0: центр общий с главным приёмником. Ставим его посередине между
|
||
// целью и активным VFO — тогда в окно попадают оба; не помещаются —
|
||
// отказываем (перестраивать главный ради слайса нельзя).
|
||
Half := FSampleRate * 0.475;
|
||
Dist := Abs(TargetHz - ActiveVfoHz);
|
||
if Dist > Half * 2 * 0.95 then Exit;
|
||
NewCenter := (TargetHz + ActiveVfoHz) / 2;
|
||
SetCenter(NewCenter); // несёт Changed(rfCenterFreq) + сдвиги слайсов
|
||
SetSliceTarget(Id, TargetHz); // SliceFreqChanged — внутри: rfCenterFreq
|
||
// флаги не перезаливает, нужен свой
|
||
Result := True;
|
||
end;
|
||
end;
|
||
|
||
{ ---- Панадаптеры на аппаратных DDC (этап 3.1) ---- }
|
||
|
||
procedure TRadioController.ResetPanDDCMaps;
|
||
var i: Integer;
|
||
begin
|
||
for i := 1 to MAX_PANS - 1 do
|
||
begin
|
||
FPanDDCIdx[i] := -1;
|
||
FPanFreqHz[i] := 0;
|
||
FPanRateKHz[i] := 0;
|
||
FPanADC[i] := 0;
|
||
FPanZoomF[i] := 0.0;
|
||
FPanPanSl[i] := 0.5;
|
||
end;
|
||
for i := 0 to MAX_DDCS - 1 do
|
||
FDDCPanMap[i] := -1;
|
||
end;
|
||
|
||
function TRadioController.PanRateAllowed(RateKHz: Word): Boolean;
|
||
// Полная сетка железа (Caps.RatePresets) минус некратные 48 кГц: движок
|
||
// требует целого отношения rate пана к аудио-rate (блок/WDSP-каналы).
|
||
var i: Integer;
|
||
begin
|
||
Result := False;
|
||
if not Assigned(FNetwork) then Exit;
|
||
if (RateKHz = 0) or ((Integer(RateKHz) * 1000) mod 48000 <> 0) then Exit;
|
||
for i := 0 to High(FNetwork.Caps.RatePresets) do
|
||
if FNetwork.Caps.RatePresets[i] = Integer(RateKHz) * 1000 then Exit(True);
|
||
end;
|
||
|
||
function TRadioController.AddPanDDC(PanId: Integer; FreqHz: Double;
|
||
RateKHz: Word; ADCSource: Byte): Boolean;
|
||
var
|
||
i, ddc: Integer;
|
||
begin
|
||
Result := False;
|
||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||
if FPanDDCIdx[PanId] >= 0 then Exit; // уже создан
|
||
if not (Assigned(FNetwork) and FRunning) then Exit;
|
||
if PanId >= FNetwork.Caps.MaxPans then Exit; // бэкенд не умеет (Pluto=1)
|
||
|
||
if not PanRateAllowed(RateKHz) then RateKHz := 384;
|
||
|
||
// hw-индекс: следующий свободный после главного DDC (слот-стабильности не
|
||
// нужно — hw-индекс скрыт за PanId, наружу торчит только PanId).
|
||
ddc := -1;
|
||
for i := FActiveDDC + 1 to FActiveDDC + MAX_PANS - 1 do
|
||
if (i < MAX_DDCS) and (FDDCPanMap[i] < 0) then begin ddc := i; Break; end;
|
||
if ddc < 0 then Exit;
|
||
|
||
// Движок: аккумулятор + analyzer пана (до включения DDC — к приходу первого
|
||
// пакета пан уже готов; PushPanIQ без пана просто дропает).
|
||
if FWDSPReady and not FDSPEngine.AddPan(PanId, Integer(RateKHz) * 1000) then
|
||
Exit;
|
||
|
||
FPanDDCIdx[PanId] := ddc;
|
||
FPanFreqHz[PanId] := FreqHz;
|
||
FPanRateKHz[PanId] := RateKHz;
|
||
FPanADC[PanId] := ADCSource;
|
||
FPanZoomF[PanId] := 0.0;
|
||
FPanPanSl[PanId] := 0.5;
|
||
FDDCPanMap[ddc] := PanId;
|
||
// В железо — IF-частота: FPanFreqHz хранит видимую (как FCenterFreq).
|
||
FNetwork.SetPanDDC(ddc, True, XvtrTranslate(FreqHz), RateKHz, ADCSource);
|
||
Result := True;
|
||
end;
|
||
|
||
function TRadioController.SetPanDDCRate(PanId: Integer; RateKHz: Word): Boolean;
|
||
var
|
||
i: Integer;
|
||
LoHz, HiHz: Double;
|
||
begin
|
||
Result := False;
|
||
if not PanDDCActive(PanId) then Exit;
|
||
if not PanRateAllowed(RateKHz) then Exit;
|
||
if FPanRateKHz[PanId] = RateKHz then Exit(True);
|
||
// Движок первым: под FSliceLock пересобирает аккумулятор/анализатор и
|
||
// переоткрывает слайс-каналы; провалился — состояние не трогаем.
|
||
if FWDSPReady and not FDSPEngine.SetPanRate(PanId, Integer(RateKHz) * 1000) then
|
||
Exit;
|
||
FPanRateKHz[PanId] := RateKHz;
|
||
// DDC железа: реконфиг rate (RebuildDDCSpecific + freq-слово, IF при XVTR).
|
||
if Assigned(FNetwork) then
|
||
FNetwork.SetPanDDC(FPanDDCIdx[PanId], True, XvtrTranslate(FPanFreqHz[PanId]),
|
||
RateKHz, FPanADC[PanId]);
|
||
// Слайсы, выпавшие из сузившейся полосы, — к её краям (абс. TargetHz + NCO).
|
||
CapturedSpan(LoHz, HiHz, PanId);
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
|
||
begin
|
||
if FSlices[i].TargetHz < LoHz then SetSliceTarget(FSlices[i].Id, LoHz);
|
||
if FSlices[i].TargetHz > HiHz then SetSliceTarget(FSlices[i].Id, HiHz);
|
||
end;
|
||
Result := True;
|
||
end;
|
||
|
||
function TRadioController.MovePanDDC(PanId, NewDDC: Integer): Boolean;
|
||
// Тот же пан (движок/анализатор/слайсы не трогаем) — только другой аппаратный
|
||
// DDC: старый гасим, новый включаем с теми же freq/rate/ADC. Короткий разрыв IQ
|
||
// на время реконфига — норма.
|
||
begin
|
||
Result := False;
|
||
if not PanDDCActive(PanId) then Exit;
|
||
if not Assigned(FNetwork) then Exit;
|
||
if (NewDDC <= FActiveDDC) or (NewDDC > MaxDDCIndex) then Exit;
|
||
if FPanDDCIdx[PanId] = NewDDC then Exit(True);
|
||
if FDDCPanMap[NewDDC] >= 0 then Exit; // занят другим паном
|
||
|
||
FNetwork.SetPanDDC(FPanDDCIdx[PanId], False, 0, 0, 0);
|
||
FDDCPanMap[FPanDDCIdx[PanId]] := -1;
|
||
// Seq-трекинг обоих индексов начинаем заново (новый поток — новый счётчик).
|
||
if FPanDDCIdx[PanId] <= High(FDDCSeqValid) then
|
||
FDDCSeqValid[FPanDDCIdx[PanId]] := False;
|
||
if NewDDC <= High(FDDCSeqValid) then
|
||
FDDCSeqValid[NewDDC] := False;
|
||
|
||
FPanDDCIdx[PanId] := NewDDC;
|
||
FDDCPanMap[NewDDC] := PanId;
|
||
FNetwork.SetPanDDC(NewDDC, True, XvtrTranslate(FPanFreqHz[PanId]),
|
||
FPanRateKHz[PanId], FPanADC[PanId]);
|
||
Result := True;
|
||
end;
|
||
|
||
function TRadioController.PanDDCIndex(PanId: Integer): Integer;
|
||
begin
|
||
if (PanId >= 1) and (PanId < MAX_PANS) then Result := FPanDDCIdx[PanId]
|
||
else if PanId = 0 then Result := FActiveDDC
|
||
else Result := -1;
|
||
end;
|
||
|
||
function TRadioController.MaxDDCIndex: Integer;
|
||
// Железо openHPSDR (Orion/Saturn) поднимает DDC0..DDC7 — DDC8 и выше молчат
|
||
// (проверено на плате: enable-бит ставится, поток не идёт).
|
||
begin
|
||
Result := 7;
|
||
end;
|
||
|
||
function TRadioController.DDCFree(DDCIdx: Integer): Boolean;
|
||
begin
|
||
Result := (DDCIdx > FActiveDDC) and (DDCIdx <= MaxDDCIndex)
|
||
and (FDDCPanMap[DDCIdx] < 0);
|
||
end;
|
||
|
||
procedure TRadioController.RemovePanDDC(PanId: Integer);
|
||
var ddc, i: Integer;
|
||
begin
|
||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||
ddc := FPanDDCIdx[PanId];
|
||
if ddc < 0 then Exit;
|
||
// Сначала снимаем роутинг (OnDDCIQ в сетевом потоке перестаёт брать пакеты
|
||
// этого DDC), потом слайсы пана (их звуковухи), потом движок и DDC в железе.
|
||
FDDCPanMap[ddc] := -1;
|
||
FPanDDCIdx[PanId] := -1;
|
||
FPanFreqHz[PanId] := 0;
|
||
FPanRateKHz[PanId] := 0;
|
||
FPanADC[PanId] := 0;
|
||
FPanZoomF[PanId] := 0.0;
|
||
FPanPanSl[PanId] := 0.5;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
|
||
RemoveSlice(FSlices[i].Id);
|
||
if FWDSPReady then FDSPEngine.RemovePan(PanId);
|
||
if Assigned(FNetwork) then FNetwork.SetPanDDC(ddc, False, 0, 0, 0);
|
||
end;
|
||
|
||
procedure TRadioController.SetPanDDCFreq(PanId: Integer; FreqHz: Double);
|
||
var i: Integer;
|
||
begin
|
||
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
|
||
if FPanDDCIdx[PanId] < 0 then Exit;
|
||
FPanFreqHz[PanId] := FreqHz; // видимая; в железо — IF при активном XVTR
|
||
if Assigned(FNetwork) then
|
||
FNetwork.SetPanDDCFreq(FPanDDCIdx[PanId], XvtrTranslate(FreqHz));
|
||
// Слайсы пана держат абсолютный TargetHz — обновляем их NCO-сдвиги
|
||
// относительно нового центра (аналог RefreshSliceShifts главного).
|
||
if Assigned(FDSPEngine) then
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
|
||
FDSPEngine.SetSliceShift(FSlices[i].Id, FSlices[i].TargetHz - FreqHz);
|
||
end;
|
||
|
||
procedure TRadioController.VisibleFreqBounds(out LoHz, HiHz: Double);
|
||
begin
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
|
||
begin
|
||
// Активный XVTR: видимые частоты живут в его диапазоне, в железо уходит
|
||
// IF (XvtrTranslate) — клампить видимую частоту пределами АЦП нельзя.
|
||
LoHz := FXvtrSettings.Entries[FCurrentXvtr].FreqBegin;
|
||
HiHz := FXvtrSettings.Entries[FCurrentXvtr].FreqEnd;
|
||
end
|
||
else if Assigned(FNetwork) then
|
||
begin
|
||
LoHz := FNetwork.Caps.MinFreqHz;
|
||
HiHz := FNetwork.Caps.MaxFreqHz;
|
||
end
|
||
else
|
||
begin
|
||
LoHz := 0;
|
||
HiHz := 61440000;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.RefreshPanDDCs;
|
||
var
|
||
p, i: Integer;
|
||
LoV, HiV, LoHz, HiHz: Double;
|
||
begin
|
||
VisibleFreqBounds(LoV, HiV);
|
||
for p := 1 to MAX_PANS - 1 do
|
||
if PanDDCActive(p) then
|
||
begin
|
||
// Видимый центр пана — в новое допустимое окно, freq-слово DDC — через
|
||
// свежую трансляцию (вход/выход XVTR, правка LO/LOError).
|
||
FPanFreqHz[p] := EnsureRange(FPanFreqHz[p], LoV, HiV);
|
||
if Assigned(FNetwork) then
|
||
FNetwork.SetPanDDCFreq(FPanDDCIdx[p], XvtrTranslate(FPanFreqHz[p]));
|
||
// Слайсы держат абсолютный видимый TargetHz: выпавшие из полосы захвата
|
||
// пана — к её краям (SetSliceTarget обновит NCO), остальные — свежий NCO.
|
||
CapturedSpan(LoHz, HiHz, p);
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = p) then
|
||
begin
|
||
if FSlices[i].TargetHz < LoHz then SetSliceTarget(FSlices[i].Id, LoHz)
|
||
else if FSlices[i].TargetHz > HiHz then SetSliceTarget(FSlices[i].Id, HiHz)
|
||
else if Assigned(FDSPEngine) then
|
||
FDSPEngine.SetSliceShift(FSlices[i].Id,
|
||
FSlices[i].TargetHz - FPanFreqHz[p]);
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
function TRadioController.PanDDCActive(PanId: Integer): Boolean;
|
||
begin
|
||
Result := (PanId >= 1) and (PanId < MAX_PANS) and (FPanDDCIdx[PanId] >= 0);
|
||
end;
|
||
|
||
function TRadioController.PanDDCFreq(PanId: Integer): Double;
|
||
begin
|
||
if PanDDCActive(PanId) then Result := FPanFreqHz[PanId]
|
||
else Result := 0;
|
||
end;
|
||
|
||
function TRadioController.PanDDCRateKHz(PanId: Integer): Word;
|
||
begin
|
||
if PanDDCActive(PanId) then Result := FPanRateKHz[PanId]
|
||
else Result := 0;
|
||
end;
|
||
|
||
procedure TRadioController.SetAnt936x(const A: TAD936xAnt);
|
||
// Новая таблица «диапазон → разъём» из настроек: сохранить + применить текущий.
|
||
begin
|
||
FAnt936x := A;
|
||
if FDevConnected then
|
||
begin
|
||
FSettings.SaveAnt936x(FDevMAC, FAnt936x);
|
||
FSettings.Save;
|
||
end;
|
||
ApplyAntPorts;
|
||
end;
|
||
|
||
procedure TRadioController.ApplyAntPorts;
|
||
// Антенна — свойство диапазона (как RxAnt/TxAnt у Alex): при смене диапазона или
|
||
// входе/выходе из трансвертера включаем разъёмы, выбранные для этого контекста.
|
||
// RX-разъём берём по диапазону ПРИЁМА, TX — по диапазону ПЕРЕДАЧИ: с мультислайс-TX
|
||
// (или split) передача может идти не на частоте центра приёма.
|
||
var
|
||
Rx, Tx: Byte;
|
||
BiRx, BiTx: Integer;
|
||
begin
|
||
if not (Assigned(FNetwork) and FNetwork.Connected) then Exit;
|
||
if not FNetwork.Caps.HasAntSwitch then Exit;
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
|
||
begin
|
||
Rx := FAnt936x.XvtrRxAnt[FCurrentXvtr];
|
||
Tx := FAnt936x.XvtrTxAnt[FCurrentXvtr];
|
||
end
|
||
else
|
||
begin
|
||
// Диапазоны — по ВИДИМЫМ частотам (VU-план AD936x).
|
||
BiRx := FreqToBandIdx(FCenterFreq, IsPluto);
|
||
if (BiRx < 0) or (BiRx >= CFG_BAND_COUNT) then Exit; // вне плана — не трогаем
|
||
Rx := FAnt936x.RxAnt[BiRx];
|
||
BiTx := FreqToBandIdx(ActiveTXFreqHz, IsPluto);
|
||
if (BiTx < 0) or (BiTx >= CFG_BAND_COUNT) then BiTx := BiRx; // TX вне плана — как RX
|
||
Tx := FAnt936x.TxAnt[BiTx];
|
||
end;
|
||
FNetwork.SetAntPorts(Rx, Tx);
|
||
end;
|
||
|
||
function TRadioController.PanADC(PanId: Integer): Byte;
|
||
begin
|
||
if PanId = 0 then Result := FMainADCSrc
|
||
else if PanDDCActive(PanId) then Result := FPanADC[PanId]
|
||
else Result := 0;
|
||
end;
|
||
|
||
function TRadioController.SetPanADC(PanId: Integer; ADC: Byte): Boolean;
|
||
begin
|
||
Result := False;
|
||
if not (Assigned(FNetwork) and FRunning) then Exit;
|
||
if ADC >= FNetwork.Caps.NumADCs then Exit;
|
||
if PanId = 0 then
|
||
begin
|
||
// Главный DDC: тот же ConfigureDDCs, что при смене rate (кэш+RebuildDDCSpecific).
|
||
if FMainADCSrc = ADC then Exit(True);
|
||
FMainADCSrc := ADC;
|
||
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, ADC,
|
||
FDitherEnabled, FRandomEnabled);
|
||
Exit(True);
|
||
end;
|
||
if not PanDDCActive(PanId) then Exit;
|
||
if FPanADC[PanId] = ADC then Exit(True);
|
||
FPanADC[PanId] := ADC;
|
||
FNetwork.SetPanDDC(FPanDDCIdx[PanId], True, XvtrTranslate(FPanFreqHz[PanId]),
|
||
FPanRateKHz[PanId], ADC);
|
||
Result := True;
|
||
end;
|
||
|
||
function TRadioController.AddSlice(TargetHz: Double;
|
||
Mode, FilterLo, FilterHi: Integer; AGC: TWDSPAGCMode; Vol: Double;
|
||
DevIndex: Integer; const DevName: string; APanId: Integer): Integer;
|
||
var
|
||
i, slot, Id: Integer;
|
||
DevIndex_: Integer;
|
||
DevName_: string;
|
||
AO: TAudioOutput;
|
||
begin
|
||
Result := -1;
|
||
Mode := EnsureRange(Mode, MODE_MIN, MODE_MAX);
|
||
if not Assigned(FDSPEngine) then Exit;
|
||
if (APanId < 0) or (APanId >= MAX_PANS) then Exit;
|
||
if (APanId > 0) and (FPanDDCIdx[APanId] < 0) then Exit; // пан не создан
|
||
if not SliceFitsCapture(TargetHz, APanId) then Exit; // вне полосы пана
|
||
|
||
slot := -1;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if not FSlices[i].Active then begin slot := i; Break; end;
|
||
if slot < 0 then Exit; // нет свободных слотов
|
||
|
||
// Устройство слота (настройка вкладки Slices) сильнее того, что попросил
|
||
// вызывающий: слот — это буква флага, и звук у неё всегда один и тот же.
|
||
if FSliceOutDev[slot] <> '' then
|
||
begin
|
||
DevName_ := FSliceOutDev[slot];
|
||
DevIndex_ := -1;
|
||
if Assigned(FAudioOut) then DevIndex_ := FAudioOut.FindDeviceByName(DevName_);
|
||
end
|
||
else
|
||
begin
|
||
DevName_ := DevName;
|
||
DevIndex_ := DevIndex;
|
||
end;
|
||
|
||
Id := FNextSliceId;
|
||
if not FDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi,
|
||
TargetHz - PanCenterHz(APanId), Vol, AGC,
|
||
APanId) then Exit;
|
||
Inc(FNextSliceId);
|
||
|
||
// Свой аудио-выход слайса на выбранной звуковухе.
|
||
AO := TAudioOutput.Create(48000);
|
||
AO.OutputBufferSize := FSettings.LoadAudioBufferSize;
|
||
AO.DeviceIndex := DevIndex_;
|
||
if FLocalAudio then AO.Open;
|
||
|
||
FSlices[slot].Active := True;
|
||
FSlices[slot].Id := Id;
|
||
// Буква флага. Слайс A — это главный приёмник (не в FSlices[]), поэтому доп.
|
||
// слайсы нумеруются с 'B' (slot 0 → B, 1 → C, …), иначе получалось два «A».
|
||
FSlices[slot].Letter := Chr(Ord('B') + slot);
|
||
FSlices[slot].PanId := APanId;
|
||
FSlices[slot].TargetHz := TargetHz;
|
||
FSlices[slot].Mode := Mode;
|
||
FSlices[slot].FilterLo := FilterLo;
|
||
FSlices[slot].FilterHi := FilterHi;
|
||
FSlices[slot].AGC := AGC;
|
||
FSlices[slot].Volume := Vol;
|
||
FSlices[slot].Muted := False;
|
||
FSlices[slot].RxMuteOnTx := True; // себя на передаче по умолчанию не слышим
|
||
FSlices[slot].FMSQOn := False; // squelch по умолчанию выключен
|
||
FSlices[slot].FMSQLevel := 40;
|
||
ClearTXProfModeTable(FSlices[slot].TXProfByMode); // звучит активный профиль
|
||
FSlices[slot].NRMode := 0; // DSP слайса стартует выключенным
|
||
FSlices[slot].NBMode := 0;
|
||
FSlices[slot].SNB := False;
|
||
FSlices[slot].ANF := False;
|
||
FSlices[slot].DeviceIndex := DevIndex_;
|
||
FSlices[slot].DevName := DevName_;
|
||
FSlices[slot].Audio := AO;
|
||
// Вход слайса по умолчанию общий (FAudioIn); свой ставится SetSliceInDevice.
|
||
FSlices[slot].InDeviceIndex := -1;
|
||
FSlices[slot].InDevName := '';
|
||
FSlices[slot].AudioIn := nil;
|
||
FSlices[slot].DMR := nil;
|
||
if Mode = MODE_DMR then
|
||
FSlices[slot].DMR := TDMRSliceDecoder.Create(Self, Id);
|
||
|
||
// Вход слота (настройка вкладки Slices) — тем же правилом, что и выход.
|
||
if (FSliceInDev[slot] <> '') and Assigned(FAudioIn) then
|
||
SetSliceInDeviceIdx(slot, FAudioIn.FindDeviceByName(FSliceInDev[slot]),
|
||
FSliceInDev[slot]);
|
||
|
||
Result := Id;
|
||
Changed(rfDevice);
|
||
end;
|
||
|
||
procedure TRadioController.RemoveSlice(Id: Integer);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
// Удаляемый слайс был источником TX — вернуть передачу на главный VFO.
|
||
if FTxSliceId = Id then SetTxSlice(0);
|
||
if Assigned(FDSPEngine) then FDSPEngine.RemoveSlice(Id);
|
||
FreeAndNil(FSlices[idx].DMR);
|
||
if FSlices[idx].Audio <> nil then
|
||
begin
|
||
FSlices[idx].Audio.Close;
|
||
FreeAndNil(FSlices[idx].Audio);
|
||
end;
|
||
ReleaseMicInput(FSlices[idx].AudioIn); // поток закроется, когда уйдёт последняя ссылка
|
||
FSlices[idx].InDeviceIndex := -1;
|
||
FSlices[idx].InDevName := '';
|
||
FSlices[idx].Active := False;
|
||
Changed(rfDevice);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceTarget(Id: Integer; TargetHz: Double);
|
||
var idx: Integer; Moved: Boolean;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
Moved := FSlices[idx].TargetHz <> TargetHz;
|
||
FSlices[idx].TargetHz := TargetHz;
|
||
if Assigned(FDSPEngine) then
|
||
FDSPEngine.SetSliceShift(Id, TargetHz - PanCenterHz(FSlices[idx].PanId));
|
||
// Слайс — источник передачи ⇒ он же задаёт частоту DUC. Раньше её дошлёт бы
|
||
// первый MOX, но в телеграфе ключ замыкает прошивка без всякого MOX: DUC
|
||
// обязан стоять правильно ВСЕГДА, а не только на передаче.
|
||
if FTxSliceId = Id then PushNetworkState;
|
||
// Уведомление — здесь, а не у каждого вызывающего: слайс двигают мышью, CAT,
|
||
// TCI и бэнд-логика, и каждый забывал сказать об этом остальным (TCI-клиенты
|
||
// оставались на старой частоте после перетаскивания флага мышью).
|
||
if Moved then SliceFreqChanged(Id);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceMode(Id, Mode: Integer);
|
||
var idx, OldMode: Integer;
|
||
begin
|
||
Mode := EnsureRange(Mode, MODE_MIN, MODE_MAX);
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
OldMode := FSlices[idx].Mode;
|
||
if (Mode = MODE_DMR) and not Assigned(FSlices[idx].DMR) then
|
||
FSlices[idx].DMR := TDMRSliceDecoder.Create(Self, Id);
|
||
FSlices[idx].Mode := Mode;
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceMode(Id, Mode);
|
||
// DMR is receive-only. If this slice used to be the TX source, hand TX back
|
||
// to the main receiver as soon as it enters DMR. FM RAW supports flat TX.
|
||
if (Mode = MODE_DMR) and (FTxSliceId = Id) then SetTxSlice(0);
|
||
if (OldMode = MODE_DMR) and (Mode <> MODE_DMR) then
|
||
FreeAndNil(FSlices[idx].DMR);
|
||
// Этот слайс — источник передачи: передающий тракт идёт за его режимом
|
||
// (иначе после смены режима в эфир ушёл бы прежний вид модуляции). Вход FM
|
||
// RAW — внешний PCM, поэтому заодно пересматриваем источник модуляции.
|
||
// Профиль этого слайса в НОВОМ режиме — до пуша цепи (см. SetMode).
|
||
if (FTxSliceId = Id) and (Mode <> OldMode) then ApplyBoundTXProfile;
|
||
if (FTxSliceId = Id) and (Mode <> OldMode) and
|
||
FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetTXMode(ActiveTXMode);
|
||
if (Mode = MODE_FMRAW) or (OldMode = MODE_FMRAW) then
|
||
begin
|
||
if Mode = MODE_FMRAW then
|
||
begin
|
||
FWebMicActive := False;
|
||
FTCIMicActive := False;
|
||
end;
|
||
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||
if Assigned(ActiveMicInput) and ActiveMicInput.IsOpen then
|
||
ActiveMicInput.Flush;
|
||
end;
|
||
end;
|
||
if FTxSliceId = Id then SyncCWKeyer;
|
||
SliceStateChanged(Id);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceFilter(Id, Low, High: Integer);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].FilterLo := Low;
|
||
FSlices[idx].FilterHi := High;
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceFilter(Id, Low, High);
|
||
SliceStateChanged(Id);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceAGCMode(Id: Integer; AGC: TWDSPAGCMode);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].AGC := AGC;
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceAGC(Id, AGC);
|
||
SliceStateChanged(Id);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceVolume(Id: Integer; Vol: Double);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].Volume := Vol;
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceVolume(Id, Vol);
|
||
SliceStateChanged(Id);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceMute(Id: Integer; Mute: Boolean);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].Muted := Mute;
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceMute(Id, Mute);
|
||
SliceStateChanged(Id);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceTapWanted(Id: Integer; V: Boolean);
|
||
begin
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetSliceTapWanted(Id, V);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceRxMuteOnTx(Id: Integer; On_: Boolean);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].RxMuteOnTx := On_;
|
||
end;
|
||
|
||
// «Главный» слайс пана = созданный на нём первым. Id монотонно растёт
|
||
// (FNextSliceId), поэтому первый = слайс с минимальным Id. 0 = слайсов нет.
|
||
function TRadioController.PanPrimarySliceId(PanId: Integer): Integer;
|
||
var i: Integer;
|
||
begin
|
||
Result := 0;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = PanId) and
|
||
((Result = 0) or (FSlices[i].Id < Result)) then
|
||
Result := FSlices[i].Id;
|
||
end;
|
||
|
||
function TRadioController.PanHasFMSlice(PanId: Integer): Boolean;
|
||
// Пан «канальный», если хотя бы один его слайс сидит в FM/DMR/FMRAW: под это
|
||
// UI рисует сетку частот по шагу FFMStepIdx (как на главном панадаптере).
|
||
var i: Integer;
|
||
begin
|
||
Result := False;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = PanId) and
|
||
(FSlices[i].Mode in [MODE_FM, MODE_DMR, MODE_FMRAW]) then
|
||
begin
|
||
Result := True;
|
||
Exit;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceDSP(Id, NRMode, NBMode: Integer;
|
||
SNBOn, ANFOn: Boolean);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].NRMode := NRMode;
|
||
FSlices[idx].NBMode := NBMode;
|
||
FSlices[idx].SNB := SNBOn;
|
||
FSlices[idx].ANF := ANFOn;
|
||
if Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetSliceNRMode(Id, NRMode);
|
||
FDSPEngine.SetSliceNBMode(Id, NBMode);
|
||
FDSPEngine.SetSliceSNB(Id, SNBOn);
|
||
FDSPEngine.SetSliceANF(Id, ANFOn);
|
||
end;
|
||
SliceStateChanged(Id);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceFMSquelch(Id: Integer; On_: Boolean; Level: Integer);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSlices[idx].FMSQOn := On_;
|
||
FSlices[idx].FMSQLevel := Level;
|
||
if Assigned(FDSPEngine) then
|
||
FDSPEngine.SetSliceFMSquelch(Id, On_, Level);
|
||
SliceStateChanged(Id);
|
||
end;
|
||
|
||
function TRadioController.SliceSMeter(Id: Integer): Double;
|
||
// Слайс может стоять на другом диапазоне (паны N кросс-банд), поэтому офсет
|
||
// берём по ЕГО видимой частоте, а не по главному диапазону.
|
||
var idx: Integer;
|
||
begin
|
||
Result := -130.0;
|
||
if not Assigned(FDSPEngine) then Exit;
|
||
Result := FDSPEngine.GetSliceSMeter(Id);
|
||
idx := FindSliceIndex(Id);
|
||
if idx >= 0 then Result := Result + SMeterCalFor(FSlices[idx].TargetHz);
|
||
end;
|
||
|
||
function TRadioController.ReadSMeterDBm: Double;
|
||
begin
|
||
if not Assigned(FDSPEngine) then Exit(-130.0);
|
||
Result := FDSPEngine.GetSMeterDBm + SMeterCalFor(ActiveVfoHz);
|
||
end;
|
||
|
||
function TRadioController.CalLookup(VisibleHz: Double;
|
||
const BandTbl: array of Double; const XvtrTbl: array of Double): Double;
|
||
// Трансвертер имеет приоритет над диапазоном: его окно частот — видимые
|
||
// (не IF), и усиление тракта задаёт конвертер/LNB, а не КВ-вход трансивера.
|
||
// Индекс диапазона — по плану активного железа (HF openHPSDR / VHF-UHF Pluto).
|
||
var i, Bi: Integer;
|
||
begin
|
||
Result := 0.0;
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
|
||
and FXvtrSettings.Entries[FCurrentXvtr].Enabled
|
||
and (VisibleHz >= FXvtrSettings.Entries[FCurrentXvtr].FreqBegin)
|
||
and (VisibleHz <= FXvtrSettings.Entries[FCurrentXvtr].FreqEnd) then
|
||
Exit(XvtrTbl[FCurrentXvtr]);
|
||
for i := 0 to CFG_XVTR_COUNT-1 do
|
||
if FXvtrSettings.Entries[i].Enabled
|
||
and (VisibleHz >= FXvtrSettings.Entries[i].FreqBegin)
|
||
and (VisibleHz <= FXvtrSettings.Entries[i].FreqEnd) then
|
||
Exit(XvtrTbl[i]);
|
||
Bi := FreqToBandIdx(VisibleHz, IsPluto);
|
||
if (Bi >= 0) and (Bi < CFG_BAND_COUNT) then Result := BandTbl[Bi];
|
||
end;
|
||
|
||
function TRadioController.SMeterCalFor(VisibleHz: Double): Double;
|
||
begin
|
||
Result := CalLookup(VisibleHz, FCal.SMeterBandDB, FCal.SMeterXvtrDB)
|
||
+ FrontEndCalDB;
|
||
end;
|
||
|
||
function TRadioController.FrontEndCalDB: Double;
|
||
// Поправка на регулируемое усиление входного тракта — то, что меняется ручкой
|
||
// или само и не должно двигать абсолютную шкалу dBm.
|
||
// openHPSDR: шаговый аттенюатор ADC режет вход → его дБ возвращаем в
|
||
// показания (так же делают Thetis и piHPSDR).
|
||
// AD936x: hardwaregain поднимает уровень (а в режимах AGC железо крутит
|
||
// его само) → вычитаем текущее значение.
|
||
// Постоянная часть тракта (LNB, кабель, конвертер) остаётся за таблицами
|
||
// калибровки на диапазон/трансвертер.
|
||
var G: Double;
|
||
begin
|
||
if not IsPluto then Exit(FAtten);
|
||
Result := 0.0;
|
||
if FNetwork = nil then Exit;
|
||
G := FNetwork.RxGainNowDb;
|
||
if G > TELEMETRY_NONE then Result := -G;
|
||
end;
|
||
|
||
function TRadioController.DispCalFor(PanId: Integer): Double;
|
||
// Зовётся из display-потока движка раз в кадр. Главный дисплей калибруем по
|
||
// центру полосы, доп. пан — по своей частоте (паны бывают кросс-банд).
|
||
var Hz: Double;
|
||
begin
|
||
if (PanId > 0) and (PanId < MAX_PANS) then Hz := FPanFreqHz[PanId]
|
||
else Hz := FCenterFreq;
|
||
if Hz <= 0 then Exit(0.0);
|
||
Result := CalLookup(Hz, FCal.DispBandDB, FCal.DispXvtrDB) + FrontEndCalDB;
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceDevice(Id, DevIndex: Integer;
|
||
const DevName: string);
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
FSliceOutDev[idx] := DevName; // выбор во флаге = устройство слота (одна истина)
|
||
FSlices[idx].DeviceIndex := DevIndex;
|
||
FSlices[idx].DevName := DevName;
|
||
if FSlices[idx].Audio <> nil then
|
||
begin
|
||
FSlices[idx].Audio.Close;
|
||
FSlices[idx].Audio.DeviceIndex := DevIndex;
|
||
if FLocalAudio then FSlices[idx].Audio.Open;
|
||
end;
|
||
end;
|
||
|
||
// --- Пул входных потоков -----------------------------------------------------
|
||
|
||
function TRadioController.AcquireMicInput(DevIndex: Integer): TAudioInput;
|
||
// Ссылка на открытый поток устройства DevIndex (создаёт при первом обращении).
|
||
// DevIndex<0 = «своего входа нет» → nil (читатель упадёт на общий FAudioIn).
|
||
var
|
||
i, n: Integer;
|
||
AI: TAudioInput;
|
||
begin
|
||
Result := nil;
|
||
if DevIndex < 0 then Exit;
|
||
// Общий вход уже открыт на этом же устройстве — переиспользуем его: второй
|
||
// Open того же PA-устройства на эксклюзивном ALSA-hw просто не прошёл бы.
|
||
if Assigned(FAudioIn) and (FAudioIn.DeviceIndex = DevIndex) then
|
||
Exit(FAudioIn);
|
||
for i := 0 to High(FMicPool) do
|
||
if FMicPool[i].DevIdx = DevIndex then
|
||
begin
|
||
// Запись с Refs=0 — поток закрыт, но объект жив (см. ReleaseMicInput).
|
||
if (FMicPool[i].Refs <= 0) and FLocalAudio and
|
||
(not FMicPool[i].AI.IsOpen) then
|
||
try FMicPool[i].AI.Open; except end;
|
||
Inc(FMicPool[i].Refs);
|
||
Exit(FMicPool[i].AI);
|
||
end;
|
||
AI := TAudioInput.Create(48000);
|
||
AI.DeviceIndex := DevIndex;
|
||
if FLocalAudio then
|
||
try AI.Open; except end;
|
||
n := Length(FMicPool);
|
||
SetLength(FMicPool, n + 1);
|
||
FMicPool[n].DevIdx := DevIndex;
|
||
FMicPool[n].Refs := 1;
|
||
FMicPool[n].AI := AI;
|
||
Result := AI;
|
||
end;
|
||
|
||
procedure TRadioController.ReleaseMicInput(var AI: TAudioInput);
|
||
// Последняя ссылка ушла — поток ЗАКРЫВАЕМ, но объект оставляем в пуле до
|
||
// выхода: TX-поток (PullSoundCardMic) мог взять указатель прямо сейчас, а
|
||
// освобождение памяти под ним = падение. Повторный выбор устройства просто
|
||
// переоткроет ту же запись.
|
||
var i: Integer;
|
||
begin
|
||
if AI = nil then Exit;
|
||
if AI = FAudioIn then begin AI := nil; Exit; end; // общий вход не из пула
|
||
for i := 0 to High(FMicPool) do
|
||
if FMicPool[i].AI = AI then
|
||
begin
|
||
Dec(FMicPool[i].Refs);
|
||
if FMicPool[i].Refs <= 0 then
|
||
begin
|
||
FMicPool[i].Refs := 0;
|
||
FMicPool[i].AI.Close;
|
||
end;
|
||
Break;
|
||
end;
|
||
AI := nil;
|
||
end;
|
||
|
||
procedure TRadioController.FreeMicPool;
|
||
var i: Integer;
|
||
begin
|
||
for i := 0 to High(FMicPool) do
|
||
if FMicPool[i].AI <> nil then
|
||
begin
|
||
FMicPool[i].AI.Close;
|
||
FMicPool[i].AI.Free;
|
||
end;
|
||
SetLength(FMicPool, 0);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceInDevice(Id, DevIndex: Integer;
|
||
const DevName: string);
|
||
begin
|
||
SetSliceInDeviceIdx(FindSliceIndex(Id), DevIndex, DevName);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceInDeviceIdx(Idx, DevIndex: Integer;
|
||
const DevName: string);
|
||
begin
|
||
if (Idx < 0) or (Idx >= MAX_SLICES) or (not FSlices[Idx].Active) then Exit;
|
||
if DevName = '' then DevIndex := -1;
|
||
FSliceInDev[Idx] := DevName; // выбор во флаге = вход слота (одна истина)
|
||
ReleaseMicInput(FSlices[Idx].AudioIn);
|
||
FSlices[Idx].InDeviceIndex := DevIndex;
|
||
FSlices[Idx].InDevName := DevName;
|
||
FSlices[Idx].AudioIn := AcquireMicInput(DevIndex);
|
||
// Слайс сейчас источник TX — источник модуляции мог сменить тип
|
||
// (звуковуха ↔ микрофон радио).
|
||
if (FTxSliceId = FSlices[Idx].Id) and FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||
end;
|
||
|
||
procedure TRadioController.SetMainMicDevice(DevIndex: Integer;
|
||
const DevName: string);
|
||
var i: Integer;
|
||
begin
|
||
if Assigned(FAudioIn) then
|
||
begin
|
||
if FAudioIn.IsOpen then FAudioIn.Close;
|
||
FAudioIn.DeviceIndex := DevIndex;
|
||
if (DevIndex >= 0) and FLocalAudio then
|
||
try FAudioIn.Open; except end;
|
||
end;
|
||
FAudioInDevName := DevName;
|
||
// Слайсы могли ссылаться на общий вход (или держать свой поток на устройстве,
|
||
// которое теперь стало общим) — перепривязать все ссылки к новой картине.
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active then
|
||
begin
|
||
ReleaseMicInput(FSlices[i].AudioIn);
|
||
FSlices[i].AudioIn := AcquireMicInput(FSlices[i].InDeviceIndex);
|
||
end;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||
end;
|
||
|
||
function TRadioController.ActiveMicInput: TAudioInput;
|
||
// Вход TX-источника: свой поток слайса, иначе общий (слайс без выбора).
|
||
var idx: Integer;
|
||
begin
|
||
Result := FAudioIn;
|
||
if FTxSliceId = 0 then Exit;
|
||
idx := FindSliceIndex(FTxSliceId);
|
||
// Поток слайса мог не открыться (устройство занято/исчезло) — тогда лучше
|
||
// передавать с общего входа, чем молчать в эфир.
|
||
if (idx >= 0) and (FSlices[idx].AudioIn <> nil) and
|
||
FSlices[idx].AudioIn.IsOpen then
|
||
Result := FSlices[idx].AudioIn;
|
||
end;
|
||
|
||
function TRadioController.ActiveMicInDevName: string;
|
||
var idx: Integer;
|
||
begin
|
||
Result := FAudioInDevName;
|
||
if FTxSliceId = 0 then Exit;
|
||
idx := FindSliceIndex(FTxSliceId);
|
||
if (idx >= 0) and (FSlices[idx].InDevName <> '') then
|
||
Result := FSlices[idx].InDevName;
|
||
end;
|
||
|
||
function TRadioController.SliceCount: Integer;
|
||
var i: Integer;
|
||
begin
|
||
Result := 0;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active then Inc(Result);
|
||
end;
|
||
|
||
function TRadioController.GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(Id);
|
||
Result := idx >= 0;
|
||
if Result then S := FSlices[idx];
|
||
end;
|
||
|
||
function TRadioController.GetSliceView(Id: Integer; out V: TSliceView): Boolean;
|
||
// Читается из чужих потоков (TCI), поэтому — поле за полем и только скаляры.
|
||
// Active перечитывается после копирования: слайс могли удалить прямо во время
|
||
// снятия снимка, и тогда честнее вернуть False, чем полуживую запись.
|
||
var idx: Integer;
|
||
begin
|
||
FillChar(V, SizeOf(V), 0);
|
||
Result := False;
|
||
idx := FindSliceIndex(Id);
|
||
if idx < 0 then Exit;
|
||
V.Id := FSlices[idx].Id;
|
||
V.PanId := FSlices[idx].PanId;
|
||
V.TargetHz := FSlices[idx].TargetHz;
|
||
V.Mode := FSlices[idx].Mode;
|
||
V.FilterLo := FSlices[idx].FilterLo;
|
||
V.FilterHi := FSlices[idx].FilterHi;
|
||
V.AGC := FSlices[idx].AGC;
|
||
V.Volume := FSlices[idx].Volume;
|
||
V.Muted := FSlices[idx].Muted;
|
||
V.FMSQOn := FSlices[idx].FMSQOn;
|
||
V.FMSQLevel := FSlices[idx].FMSQLevel;
|
||
V.NRMode := FSlices[idx].NRMode;
|
||
V.NBMode := FSlices[idx].NBMode;
|
||
V.SNB := FSlices[idx].SNB;
|
||
V.ANF := FSlices[idx].ANF;
|
||
Result := FSlices[idx].Active and (FSlices[idx].Id = Id);
|
||
end;
|
||
|
||
function TRadioController.PanSliceCount(PanId: Integer): Integer;
|
||
var i: Integer;
|
||
begin
|
||
Result := 0;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = PanId) then Inc(Result);
|
||
end;
|
||
|
||
function TRadioController.PanSliceId(PanId, Index: Integer): Integer;
|
||
var i, Seen: Integer;
|
||
begin
|
||
Result := 0;
|
||
if Index < 0 then Exit;
|
||
Seen := 0;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
|
||
begin
|
||
if Seen = Index then Exit(FSlices[i].Id);
|
||
Inc(Seen);
|
||
end;
|
||
end;
|
||
|
||
function TRadioController.SlicePanIndex(Id: Integer; out PanId, Index: Integer): Boolean;
|
||
var i, Seen, Pan: Integer;
|
||
begin
|
||
Result := False;
|
||
PanId := 0; Index := 0;
|
||
if Id <= 0 then Exit;
|
||
Pan := -1;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].Id = Id) then
|
||
begin
|
||
Pan := FSlices[i].PanId;
|
||
Break;
|
||
end;
|
||
if Pan < 0 then Exit;
|
||
|
||
Seen := 0;
|
||
for i := 0 to MAX_SLICES - 1 do
|
||
if FSlices[i].Active and (FSlices[i].PanId = Pan) then
|
||
begin
|
||
if FSlices[i].Id = Id then
|
||
begin
|
||
PanId := Pan;
|
||
Index := Seen;
|
||
Exit(True);
|
||
end;
|
||
Inc(Seen);
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Слоты слайсов: настройки (CAT-порт, Auto TX) висят на слоте/букве, а слайс
|
||
// на слоте может появляться и исчезать.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
class function TRadioController.SliceSlotLetter(Slot: Integer): Char;
|
||
begin
|
||
// Слайс A — главный приёмник, поэтому слоты нумеруются с 'B' (как AddSlice).
|
||
if (Slot < 0) or (Slot >= MAX_SLICES) then Exit('?');
|
||
Result := Chr(Ord('B') + Slot);
|
||
end;
|
||
|
||
function TRadioController.SliceIdBySlot(Slot: Integer): Integer;
|
||
begin
|
||
Result := 0;
|
||
if (Slot < 0) or (Slot >= MAX_SLICES) then Exit;
|
||
if FSlices[Slot].Active then Result := FSlices[Slot].Id;
|
||
end;
|
||
|
||
function TRadioController.SliceSlotOf(Id: Integer): Integer;
|
||
begin
|
||
Result := FindSliceIndex(Id);
|
||
end;
|
||
|
||
function TRadioController.SliceSlotAutoTx(Slot: Integer): Boolean;
|
||
begin
|
||
Result := (Slot >= 0) and (Slot < MAX_SLICES) and FSliceAutoTx[Slot];
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceSlotAutoTx(Slot: Integer; On_: Boolean);
|
||
// Без Changed(): единственный вызывающий — TCATAdapter.ApplySettings, а он
|
||
// работает и на старте (до постройки виджетов), и на connect. Перерисовку
|
||
// бейджей делает UI-путь настроек (MainForm.OnSliceCATSettingsChange).
|
||
begin
|
||
if (Slot < 0) or (Slot >= MAX_SLICES) then Exit;
|
||
FSliceAutoTx[Slot] := On_;
|
||
end;
|
||
|
||
function TRadioController.SliceAutoTx(Id: Integer): Boolean;
|
||
begin
|
||
Result := SliceSlotAutoTx(FindSliceIndex(Id));
|
||
end;
|
||
|
||
function TRadioController.SliceSlotOutDev(Slot: Integer): string;
|
||
begin
|
||
if (Slot >= 0) and (Slot < MAX_SLICES) then Result := FSliceOutDev[Slot]
|
||
else Result := '';
|
||
end;
|
||
|
||
function TRadioController.SliceSlotInDev(Slot: Integer): string;
|
||
begin
|
||
if (Slot >= 0) and (Slot < MAX_SLICES) then Result := FSliceInDev[Slot]
|
||
else Result := '';
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceSlotAudio(Slot: Integer; const OutDev, InDev: string);
|
||
// Настройка = устройство слота, поэтому применяем сразу: слайс, который стоит
|
||
// на слоте, переезжает на новое устройство (как выбор во вкладке AUD флага).
|
||
var Id: Integer;
|
||
begin
|
||
if (Slot < 0) or (Slot >= MAX_SLICES) then Exit;
|
||
Id := SliceIdBySlot(Slot);
|
||
if FSliceOutDev[Slot] <> OutDev then
|
||
begin
|
||
FSliceOutDev[Slot] := OutDev;
|
||
if Id > 0 then
|
||
begin
|
||
if (OutDev <> '') and Assigned(FAudioOut) then
|
||
SetSliceDevice(Id, FAudioOut.FindDeviceByName(OutDev), OutDev)
|
||
else
|
||
SetSliceDevice(Id, -1, ''); // общий выход приложения
|
||
end;
|
||
end;
|
||
if FSliceInDev[Slot] <> InDev then
|
||
begin
|
||
FSliceInDev[Slot] := InDev;
|
||
if Id > 0 then
|
||
begin
|
||
if (InDev <> '') and Assigned(FAudioIn) then
|
||
SetSliceInDevice(Id, FAudioIn.FindDeviceByName(InDev), InDev)
|
||
else
|
||
SetSliceInDevice(Id, -1, ''); // общий вход приложения
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.RequestSliceTx(SliceId: Integer; On_: Boolean;
|
||
Tune: Boolean = False);
|
||
// PTT, пришедшая по CAT-порту слайса или по TCI с номером приёмника. Аналог
|
||
// «Auto Switch TX Slice» в SmartSDR CAT: с Auto TX слайс сам становится
|
||
// TX-источником, без него — передаёт, только если оператор уже выбрал его
|
||
// источником (бейдж TX на флаге).
|
||
// Выполняется в потоке контроллера (CAT и TCI маршалят через Invoke), поэтому
|
||
// проверка «уже в эфире» и смена источника атомарны относительно GUI.
|
||
var idx: Integer;
|
||
begin
|
||
idx := FindSliceIndex(SliceId);
|
||
if idx < 0 then Exit;
|
||
|
||
if not On_ then
|
||
begin
|
||
// Отпускание PTT снимает только СВОЮ передачу: RX; на порту слайса C не
|
||
// должен обрывать эфир слайса B.
|
||
if (FTxSliceId = SliceId) and (FTransmitting or FTuning) then
|
||
begin
|
||
if FTuning then SetTune(False) else SetMOX(False);
|
||
end;
|
||
Exit;
|
||
end;
|
||
|
||
// Передатчик один: пока кто-то в эфире (другой слайс, главный VFO, кнопка
|
||
// MOX/TUNE, аппаратная PTT) — чужую передачу не перехватываем и источник не
|
||
// переключаем. Своя же передача уже идёт, повторный TX; — no-op.
|
||
if FTransmitting or FTuning then Exit;
|
||
if FSlices[idx].Mode = MODE_DMR then Exit; // AMBE-передатчика нет
|
||
|
||
if FTxSliceId <> SliceId then
|
||
begin
|
||
if not FSliceAutoTx[idx] then Exit; // без Auto TX слайс сам себя не назначает
|
||
SetTxSlice(SliceId);
|
||
if FTxSliceId <> SliceId then Exit; // SetTxSlice отказал (DMR/нет слайса)
|
||
end;
|
||
if Tune then SetTune(True) else SetMOX(True);
|
||
end;
|
||
|
||
// Снимок кадра для web-зеркала: потолок точек задаёт настройка web.spec_pixels
|
||
// (Limit), при большем разрешении анализатора децимируем max-детектором
|
||
// (не обрезаем).
|
||
// DstCount — сколько точек РЕАЛЬНО заполнено; отправлять больше нельзя, иначе
|
||
// хвост буфера (нули = 0 dBm) уедет в эфир белой полосой, а частотная шкала
|
||
// клиента растянется на несуществующие точки.
|
||
//
|
||
// Группы — строго одинаковой ширины (целочисленный фактор). Прежнее деление
|
||
// «i*Count div N» давало на нецелом отношении соседние группы разной ширины
|
||
// (1 и 2 точки), а смещение max-детектора зависит от числа точек в группе —
|
||
// по спектру шла гребёнка ±3 дБ, в водопаде видимая как вертикальная рябь.
|
||
procedure SnapshotPixels(const Pixels: array of Single; Count: Integer;
|
||
var Dst: array of Single; Limit: Integer; out DstCount: Integer);
|
||
var
|
||
i, j, S0, S1, N, K, M: Integer;
|
||
V: Single;
|
||
begin
|
||
N := EnsureRange(Limit, 64, Length(Dst));
|
||
if Count > Length(Pixels) then Count := Length(Pixels);
|
||
if Count < 0 then Count := 0;
|
||
if Count <= N then
|
||
begin
|
||
for i := 0 to Count - 1 do Dst[i] := Pixels[i];
|
||
DstCount := Count;
|
||
Exit;
|
||
end;
|
||
K := (Count + N - 1) div N; // точек анализатора на точку web
|
||
M := (Count + K - 1) div K; // столько точек получится (<= N)
|
||
for i := 0 to M - 1 do
|
||
begin
|
||
S0 := i * K;
|
||
S1 := S0 + K - 1;
|
||
if S1 > Count - 1 then S1 := Count - 1; // хвостовая группа короче
|
||
V := Pixels[S0];
|
||
for j := S0 + 1 to S1 do
|
||
if Pixels[j] > V then V := Pixels[j];
|
||
Dst[i] := V;
|
||
end;
|
||
DstCount := M;
|
||
end;
|
||
|
||
procedure TRadioController.OnSpectrumReady(const Pixels: array of Single;
|
||
Count: Integer);
|
||
// DSP-поток: сохраняем последний кадр для web-зеркала и отдаём сырьё наружу
|
||
// (UI рисует FSpecView). Децимации у спектра нет.
|
||
begin
|
||
SnapshotPixels(Pixels, Count, FSpectrumBuf, FWebSpecPixels, FSpectrumBufCount);
|
||
if Assigned(FOnSpectrumData) then FOnSpectrumData(Pixels, Count);
|
||
end;
|
||
|
||
procedure TRadioController.OnWaterfallReady(const Pixels: array of Single;
|
||
Count: Integer);
|
||
// DSP-поток: web-зеркало получает КАЖДЫЙ кадр; децимация рендера водопада —
|
||
// забота фронтенда (OnWaterfallData).
|
||
//
|
||
// Web шлёт 20 строк/с, анализатор даёт до 60 кадров/с — просто «последний
|
||
// кадр» терял бы 2 из 3 (короткая посылка CW/FT8 могла не попасть ни в одну
|
||
// строку). Копим кадры max-hold'ом до тех пор, пока PushState не заберёт
|
||
// строку (идиома TWaterfallView.SetWaterfallData при WfFrameInterval>1).
|
||
var
|
||
Tmp: array[0..SPECTRUM_PIXELS-1] of Single;
|
||
N, i: Integer;
|
||
begin
|
||
SnapshotPixels(Pixels, Count, Tmp, FWebSpecPixels, N);
|
||
if (not FWfAccumFresh) or (N <> FWaterfallBufCount) then
|
||
begin
|
||
for i := 0 to N - 1 do FWaterfallBuf[i] := Tmp[i];
|
||
FWaterfallBufCount := N;
|
||
end
|
||
else
|
||
for i := 0 to N - 1 do
|
||
if Tmp[i] > FWaterfallBuf[i] then FWaterfallBuf[i] := Tmp[i];
|
||
FWfAccumFresh := True;
|
||
if Assigned(FOnWaterfallData) then FOnWaterfallData(Pixels, Count);
|
||
end;
|
||
|
||
procedure TRadioController.OnMicPacket(const Data: TMicDataPacket);
|
||
// HW mic-стрим уходит в WDSP только если выбран источник Radio; иначе сэмплы
|
||
// игнорируются (mic берётся со звуковой карты).
|
||
begin
|
||
if FTransmitting and FWDSPReady and
|
||
(FDSPEngine.TXMicSource = txmsRadio) then
|
||
FDSPEngine.PushTXMicSamples16(Data.Samples, 64);
|
||
end;
|
||
|
||
function TRadioController.PullSoundCardMic(MaxN: Integer): Integer;
|
||
// Колбэк TTXDSPThread при TXMicSource=txmsSoundCard. Тянем только реально
|
||
// доступные сэмплы из FAudioIn (иначе пушили бы тишину в mic-цепь WDSP),
|
||
// пачкой в локальный буфер — экономим будилки семафора.
|
||
var
|
||
i, n: Integer;
|
||
AI: TAudioInput;
|
||
Buf: array[0..2047] of Double;
|
||
begin
|
||
Result := 0;
|
||
AI := ActiveMicInput; // вход TX-слайса (свой) либо общий
|
||
if (AI = nil) or (not AI.IsOpen) then Exit;
|
||
n := AI.Available;
|
||
if n > MaxN then n := MaxN;
|
||
if n > Length(Buf) then n := Length(Buf);
|
||
for i := 0 to n - 1 do
|
||
Buf[i] := AI.ReadSample;
|
||
if n > 0 then
|
||
FDSPEngine.PushTXMicSamplesD(Buf, n);
|
||
Result := n;
|
||
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.RxGainMode := FRxGainMode; Result.RxGainDb := FRxGainDb;
|
||
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.
|
||
// QO-100 beacon lock складывает свою коррекцию дрейфа LNB ПРЯМО в LOError
|
||
// (живая калибровка): она сразу видна в настройках и сохраняется. Отдельного
|
||
// слагаемого нет — иначе двойной счёт.
|
||
Result := VisibleHz
|
||
- FXvtrSettings.Entries[FCurrentXvtr].LOOffset
|
||
+ FXvtrSettings.Entries[FCurrentXvtr].LOError;
|
||
end;
|
||
|
||
function TRadioController.XvtrTranslateTX(VisibleHz: Double): Double;
|
||
begin
|
||
Result := VisibleHz;
|
||
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
|
||
if not FXvtrSettings.Entries[FCurrentXvtr].Enabled then Exit;
|
||
// QO-100 / split-LO: TX идёт на собственный uplink-LO (visible - TxLOOffset),
|
||
// без LOError — дрейф LNB к передаче отношения не имеет.
|
||
if FXvtrSettings.Entries[FCurrentXvtr].FullDuplexTx then
|
||
// TO Error: сам транспондер-offset держим стандартным, уход тракта — здесь.
|
||
Result := VisibleHz - FXvtrSettings.Entries[FCurrentXvtr].TxLOOffset
|
||
+ FXvtrSettings.Entries[FCurrentXvtr].TxLOError
|
||
else
|
||
// Обычный трансвертер: RX=TX LO (с калибровкой LOError) — как раньше.
|
||
Result := VisibleHz
|
||
- FXvtrSettings.Entries[FCurrentXvtr].LOOffset
|
||
+ FXvtrSettings.Entries[FCurrentXvtr].LOError;
|
||
end;
|
||
|
||
function TRadioController.ActiveTXFreqHz: Double;
|
||
// Снимок (GetSliceView), а не GetSlice: функцию зовут и внешние фронтенды из
|
||
// своих потоков, а копия TCtrlSlice тащит managed-строки слайса.
|
||
var S: TSliceView;
|
||
begin
|
||
// Мультислайс-TX: если выбран слайс-источник — передаём на его частоте
|
||
// (слайсы не несут repeater-конфиг, FM-сдвиг не применяем).
|
||
if (FTxSliceId > 0) and GetSliceView(FTxSliceId, S) then
|
||
Result := S.TargetHz
|
||
else
|
||
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;
|
||
// Телеграф + TUN: несущую даёт SSB-модулятор из тона pitch, поэтому DUC надо
|
||
// сдвинуть навстречу — иначе настроечная несущая уедет от VFO на pitch. При
|
||
// работе ключом (кейер/CWX) сдвигать нечего: РЧ формирует прошивка на DUC.
|
||
if FTuning and (ActiveTXMode in [MODE_CWL, MODE_CWU]) and FWDSPReady
|
||
and Assigned(FDSPEngine) then
|
||
Result := Result + FDSPEngine.CWLOOffset(ActiveTXMode);
|
||
end;
|
||
|
||
function TRadioController.TXTuneFreqHz: Double;
|
||
// Единая дверь «какую частоту передачи класть в железо». Кроме трансвертера
|
||
// учитывает сдвиг несущей локального кейера: манипуляция идёт не на нуле
|
||
// бейсбенда, а на CWCarrierOffsetHz, значит гетеродин обязан уехать НАВСТРЕЧУ,
|
||
// иначе телеграф уйдёт в эфир мимо VFO ровно на этот сдвиг.
|
||
// ★Сдвиг применяется, пока кейер ВООРУЖЁН, а не только на передаче: железо
|
||
// может замкнуть ключ в любой момент (тот же урок, что был с частотой DUC).
|
||
//
|
||
// ★★ЗНАК — ЗАМЕРЕН НА ЭФИРЕ (Pluto, 2026-08-10), а не выведен: положительная
|
||
// частота бейсбенда в нашей конвенции выходит в эфир НИЖЕ гетеродина
|
||
// (РЧ = LO + CW_TX_BB_SIGN·f). Первая версия считала наоборот, и несущая вставала
|
||
// на 2×сдвига НИЖЕ маркера. Отсюда гетеродин уезжает ВВЕРХ на величину сдвига.
|
||
// Если когда-нибудь понадобится ненулевой сдвиг у openHPSDR — проверить там
|
||
// отдельно: тракт другой (цифровой DUC вместо zero-IF), и знак может отличаться.
|
||
const
|
||
CW_TX_BB_SIGN = -1;
|
||
begin
|
||
Result := XvtrTranslateTX(ActiveTXFreqHz) - CW_TX_BB_SIGN * CWCarrierOffsetHz;
|
||
end;
|
||
|
||
function TRadioController.ActiveTXMode: Integer;
|
||
var S: TCtrlSlice;
|
||
begin
|
||
if (FTxSliceId > 0) and GetSlice(FTxSliceId, S) then Result := S.Mode
|
||
else Result := FMode;
|
||
end;
|
||
|
||
function TRadioController.TxBandIdx: Integer;
|
||
begin
|
||
// При TX на слайсе другого диапазона (только КВ) калибровка/антенна — по
|
||
// банду TX-частоты, иначе главный банд.
|
||
Result := FCurrentBand;
|
||
if (FTxSliceId > 0) and (FCurrentXvtr < 0) then
|
||
begin
|
||
Result := FreqToBandIdx(ActiveTXFreqHz, IsPluto);
|
||
if (Result < 0) or (Result >= CFG_BAND_COUNT) then Result := FCurrentBand;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.SetTxSlice(SliceId: Integer);
|
||
var S: TCtrlSlice;
|
||
begin
|
||
if SliceId < 0 then SliceId := 0;
|
||
// Слайс-источник должен существовать (0 = главный всегда валиден).
|
||
if SliceId > 0 then
|
||
begin
|
||
if not GetSlice(SliceId, S) then Exit;
|
||
// EWSDR has no DMR transmitter/AMBE encoder.
|
||
if S.Mode = MODE_DMR then Exit;
|
||
end;
|
||
if FTxSliceId = SliceId then Exit;
|
||
// Selecting main A while it is in DMR is allowed as an RX selection, but it
|
||
// cannot inherit an active carrier from a previously selected TX slice.
|
||
if (SliceId = 0) and (FMode = MODE_DMR) then
|
||
begin
|
||
if FTuning then SetTune(False)
|
||
else if FTransmitting then SetMOX(False);
|
||
end;
|
||
FTxSliceId := SliceId;
|
||
// Профиль, привязанный к новому источнику, — ДО пуша режима и цепи: иначе
|
||
// ApplyTXSettingsToDSP внутри SelectTXProfile лёг бы на старый режим.
|
||
ApplyBoundTXProfile;
|
||
// Передающий тракт под режим нового источника (idle-канал, применится к PTT).
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetTXMode(ActiveTXMode);
|
||
// Источник модуляции — вход НОВОГО слайса (у него может быть свой).
|
||
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||
end;
|
||
// Кольцо нового входа копило звук всё время, пока он не был источником:
|
||
// без сброса первые ~200мс передачи ушли бы в эфир «из прошлого».
|
||
if Assigned(ActiveMicInput) and ActiveMicInput.IsOpen then
|
||
ActiveMicInput.Flush;
|
||
SyncCWKeyer; // кейер прошивки — под режим нового источника
|
||
FDriveLevel := CalcDriveByte; // калибровка драйва банда TX-слайса
|
||
ApplyAntPorts; // AD936x: TX-разъём — по диапазону TX-слайса
|
||
PushNetworkState; // TX-частота/Alex через ActiveTXFreqHz/TxBandIdx
|
||
Changed(rfTransmitting); // UI: перерисовать TX-бейджи флагов/оверлей
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// QO-100 beacon lock
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const
|
||
// --- контур коррекции (измерение частоты — от ДЕКОДЕРА, не от display-FFT) ---
|
||
// Декодер точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload):
|
||
// суммарный остаток ResidHz+CarFreqHz = текущее смещение маяка от точки наведения.
|
||
// Это смещение медленно стекаем в LOError активного XVTR → ретюн LO компенсирует
|
||
// дрейф LNB, весь даунлинк стоит. Внутренний NCO декодера держит маяк
|
||
// декодированным во время плавного ретюна (наведение едет feed-forward за маяком).
|
||
BEACON_GAIN = 0.5; // гейн контура: <1 — запас от перелёта (декодерная
|
||
// оценка точная, но ретюн LO имеет латентность пайплайна)
|
||
BEACON_DEADBAND_HZ = 20.0; // внутри — коррекции стоп (декодер точнее FFT → дедбэнд уже)
|
||
BEACON_MAX_STEP_HZ = 400.0; // клам один шаг коррекции (защита от выброса оценки)
|
||
BEACON_SETTLE_TICKS = 8; // пауза (~0.8 с @10 Гц) после коррекции — осадка ретюна LO
|
||
BEACON_SAVE_TICKS = 30; // троттлинг записи LOError на диск (~3 с @10 Гц)
|
||
// --- знак отображения остатка декодера ---
|
||
// invert=True (QO-100/LNB) выравнивает частотную ось декодера с дисплеем, поэтому
|
||
// display_маяка = наведение + (ResidHz+CarFreqHz). Если на эфире контур убегает —
|
||
// ось перевёрнута, флипнуть на -1.0 (см. лог: e и дрейф должны иметь один знак).
|
||
BEACON_RESID_SIGN = 1.0;
|
||
// --- метрика лока (берём из снимка декодера: CarrierLock + SNR) ---
|
||
BEACON_LOCK_SNR_DB = 3.0; // солидный лок: декодер несёт несущую и SNR выше этого
|
||
BEACON_UNLOCK_SNR_DB = 0.0; // гистерезис: лок гаснет ниже этого
|
||
// --- «это точно BPSK, а не голая несущая» ---
|
||
// CarrierLock+SNR подтверждают лишь то, что в полосе есть узкая линия и Costas
|
||
// держит фазу. Ровная несущая (телеграф, пораженка, остаток несущей у цифры) даёт
|
||
// ровно это, причём с ОТЛИЧНЫМ SNR: захваченная Costas'ом, она лежит целиком на
|
||
// оси I, Q пустой. Контур тогда честно уводил LOError, подтягивая чужой сигнал к
|
||
// опорной частоте маяка. Различаем по констелляции: у настоящего BPSK точки ходят
|
||
// между двумя сгустками ±I (данные после свёрточного кодера равновероятны), у
|
||
// несущей все точки в одном. Меряем долю меньшинства по знаку I.
|
||
BEACON_BAL_MIN = 0.15; // ниже — на BPSK не похоже, лок не даём
|
||
BEACON_BAL_DROP = 0.08; // гистерезис: уже взятый лок гаснет ниже этого
|
||
BEACON_BAL_MIN_PTS = 64; // меньше точек в кольце — судить рано
|
||
|
||
procedure TRadioController.SetBeaconRefHz(Hz: Double);
|
||
begin
|
||
FBeaconRefHz := Hz;
|
||
end;
|
||
|
||
procedure TRadioController.BeaconSeedAtHz(DisplayHz: Double);
|
||
// Пользователь кликнул по маяку → наводим узкий тракт декодера на эту частоту и
|
||
// переинициализируем петли (RunAcquisition заново оценит несущую). Дальше декодер
|
||
// сам ведёт маяк своим NCO, а ServiceBeaconLock стекает измеренный дрейф в LOError.
|
||
begin
|
||
if not FBeaconLockOn then SetBeaconLock(True);
|
||
FBeaconDecHz := DisplayHz;
|
||
FBeaconTrackHz := DisplayHz;
|
||
if Assigned(FBeaconDec) then
|
||
begin
|
||
FBeaconDec.SetOffsetHz(DisplayHz - FCenterFreq);
|
||
FBeaconDec.Reseed; // одноразовый грубый захват частоты заново
|
||
end;
|
||
FBeaconBal := 0.0; // новое наведение — BPSK-ность доказывается заново
|
||
FBeaconLocked := False;
|
||
FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать декодеру захватиться до 1-й коррекции
|
||
Changed(rfBeaconLock);
|
||
end;
|
||
|
||
procedure TRadioController.SetBeaconLock(On_: Boolean);
|
||
// Один концепт: лок = декодер + трим LOError. Включение запускает декодер маяка;
|
||
// пользователь кликает по маяку (BeaconSeedAtHz) для наведения. Когда декодер
|
||
// солидно залочен, ServiceBeaconLock стекает его измеренный дрейф в LOError.
|
||
begin
|
||
if On_ = FBeaconLockOn then Exit;
|
||
FBeaconLockOn := On_;
|
||
if On_ then
|
||
begin
|
||
FBeaconCorrCooldown := 0;
|
||
FBeaconSaveCooldown := 0;
|
||
FBeaconLocked := False;
|
||
FBeaconBal := 0.0;
|
||
FBeaconSNRdB := -99.0;
|
||
FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError несёт калибровку)
|
||
FBeaconDecHz := 0.0; // ещё не наведён — ждём клик по маяку
|
||
FBeaconTrackHz := FBeaconRefHz;
|
||
if Assigned(FBeaconDec) then
|
||
begin
|
||
FBeaconDec.SetOffsetHz(0);
|
||
FBeaconDec.SetEnabled(True); // ResetDSP внутри
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
if Assigned(FBeaconDec) then FBeaconDec.SetEnabled(False);
|
||
FBeaconDecHz := 0.0;
|
||
PersistBeaconCalibration; // финальный flush LOError на диск
|
||
end;
|
||
Changed(rfBeaconLock);
|
||
end;
|
||
|
||
function BeaconIQBalance(const S: TBeaconScope): Double;
|
||
// Доля меньшинства среди знаков I в кольце констелляции: 0.5 — идеально
|
||
// сбалансированный BPSK, 0 — все точки в одном сгустке (немодулированная
|
||
// несущая). -1 = точек мало, судить рано.
|
||
var
|
||
i, Neg: Integer;
|
||
begin
|
||
if S.Count < BEACON_BAL_MIN_PTS then Exit(-1.0);
|
||
Neg := 0;
|
||
for i := 0 to S.Count - 1 do
|
||
if S.PtI[i] < 0 then Inc(Neg);
|
||
Result := Min(Neg, S.Count - Neg) / S.Count;
|
||
end;
|
||
|
||
procedure TRadioController.ServiceBeaconLock;
|
||
// Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Эталон частоты — ДЕКОДЕР:
|
||
// он точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload), и его
|
||
// суммарный остаток ResidHz+CarFreqHz = текущее смещение маяка от точки наведения.
|
||
// Измеренную АБСОЛЮТНУЮ частоту маяка (наведение + остаток) подтягиваем к опорной,
|
||
// стекая ошибку в LOError активного XVTR → ретюн LO компенсирует дрейф LNB, весь
|
||
// даунлинк стоит. Наведение декодера едет feed-forward за маяком (FBeaconDecHz),
|
||
// так что внутренний NCO держит маяк декодированным сквозь плавный ретюн (нетто
|
||
// baseband-смещение ≈ 0: маяк сдвинулся на −dLock железом, наведение тоже на −dLock).
|
||
var
|
||
S: TBeaconScope;
|
||
residHz, measured, e, dLock, Bal: Double;
|
||
begin
|
||
if not FBeaconLockOn then Exit;
|
||
if not Assigned(FBeaconDec) then Exit;
|
||
|
||
// Держим наведение декодера через АКТУАЛЬНЫЙ FCenterFreq (панорама/CTUN не ломают
|
||
// снос). До клика (FBeaconDecHz=0) — DC; acquisition попробует поймать у центра.
|
||
if FBeaconDecHz > 0 then
|
||
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq)
|
||
else
|
||
FBeaconDec.SetOffsetHz(0);
|
||
|
||
if FCurrentXvtr < 0 then Exit; // трим LOError только в XVTR-режиме
|
||
if FBeaconSaveCooldown > 0 then Dec(FBeaconSaveCooldown);
|
||
|
||
FBeaconDec.GetScope(S);
|
||
FBeaconSNRdB := FBeaconSNRdB + 0.2 * (S.SNRdB - FBeaconSNRdB);
|
||
// Доля меньшинства по знаку I: у BPSK ≈0.5, у голой несущей ≈0. Кольцо в 256
|
||
// точек при 400 Bd набирается за ~0.6 с, EMA добавляет ещё столько же — то есть
|
||
// чужой сигнал отваливается примерно за секунду, а не за десяток (кадр FEC).
|
||
Bal := BeaconIQBalance(S);
|
||
if Bal >= 0 then FBeaconBal := FBeaconBal + 0.2 * (Bal - FBeaconBal);
|
||
// Солидный лок: декодер несёт несущую, SNR выше порога И картинка похожа на
|
||
// BPSK (гистерезис по обоим — против мерцания).
|
||
if S.CarrierLock and (FBeaconSNRdB >= BEACON_LOCK_SNR_DB)
|
||
and (FBeaconBal >= BEACON_BAL_MIN) then FBeaconLocked := True
|
||
else if (FBeaconSNRdB < BEACON_UNLOCK_SNR_DB)
|
||
or (FBeaconBal < BEACON_BAL_DROP) then FBeaconLocked := False;
|
||
|
||
// Остаток несущей декодера (Гц, относительно наведения). invert выравнивает ось
|
||
// декодера с дисплеем → display_маяка = наведение + остаток (знак BEACON_RESID_SIGN).
|
||
residHz := BEACON_RESID_SIGN * (S.ResidHz + S.CarFreqHz);
|
||
if FBeaconDecHz > 0 then
|
||
begin
|
||
measured := FBeaconDecHz + residHz; // абсолютная display-частота маяка
|
||
FBeaconTrackHz := measured; // для оранжевого маркера
|
||
end
|
||
else
|
||
measured := FBeaconTrackHz;
|
||
|
||
// Коррекция: не каждый тик (ретюн LO имеет латентность пайплайна). После коррекции
|
||
// ждём BEACON_SETTLE_TICKS. Только при солидном локе и после наведения кликом.
|
||
if FBeaconCorrCooldown > 0 then
|
||
Dec(FBeaconCorrCooldown)
|
||
else if FBeaconLocked and (FBeaconDecHz > 0) then
|
||
begin
|
||
e := measured - FBeaconRefHz;
|
||
if Abs(e) > BEACON_DEADBAND_HZ then
|
||
begin
|
||
dLock := BEACON_GAIN * e;
|
||
if dLock > BEACON_MAX_STEP_HZ then dLock := BEACON_MAX_STEP_HZ;
|
||
if dLock < -BEACON_MAX_STEP_HZ then dLock := -BEACON_MAX_STEP_HZ;
|
||
ApplyBeaconLOError(dLock); // в LOError + ретюн + троттл-сохранение
|
||
// Маяк уедет на −dLock (display); ведём наведение туда же feed-forward, чтобы
|
||
// baseband декодера не дёрнулся и лок не потерялся.
|
||
FBeaconDecHz := measured - dLock;
|
||
FBeaconTrackHz := FBeaconDecHz;
|
||
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq);
|
||
FBeaconCorrCooldown := BEACON_SETTLE_TICKS;
|
||
end;
|
||
end;
|
||
Changed(rfBeaconLock);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyBeaconLOError(dLock: Double);
|
||
// Складывает коррекцию маяка ПРЯМО в LOError активного трансвертера — это и есть
|
||
// живая калибровка дрейфа LNB: сразу видна в настройках, переживает рестарт.
|
||
// FBeaconLockHz — лишь сессионный аккумулятор для индикации. Запись на диск
|
||
// троттлим (BEACON_SAVE_TICKS): коррекции редки (deadband), но при долгом прогреве
|
||
// LNB их много — не молотим диск каждую.
|
||
begin
|
||
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
|
||
FXvtrSettings.Entries[FCurrentXvtr].LOError :=
|
||
FXvtrSettings.Entries[FCurrentXvtr].LOError + dLock;
|
||
FBeaconLockHz := FBeaconLockHz + dLock; // сколько накрутили за сессию (индикация)
|
||
PushNetworkState; // ретюн: XvtrTranslate читает новый LOError
|
||
if (FBeaconSaveCooldown = 0) and FDevConnected then
|
||
begin
|
||
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||
FBeaconSaveCooldown := BEACON_SAVE_TICKS;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.PersistBeaconCalibration;
|
||
// Коррекция уже живёт в LOError (ApplyBeaconLOError). Здесь — финальный flush на
|
||
// диск при выключении лока, на случай неотправленной из-за троттлинга.
|
||
begin
|
||
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
|
||
if FDevConnected then FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||
FBeaconSaveCooldown := 0;
|
||
end;
|
||
|
||
function TRadioController.BeaconState: TBeaconState;
|
||
begin
|
||
if not FBeaconLockOn then Exit(bsIdle);
|
||
if FBeaconLocked then Result := bsLock else Result := bsSearch;
|
||
end;
|
||
|
||
function TRadioController.BeaconCorrectionHz: Double;
|
||
begin
|
||
Result := FBeaconLockHz;
|
||
end;
|
||
|
||
function TRadioController.BeaconSNRdB: Double;
|
||
begin
|
||
Result := FBeaconSNRdB; // SNR последнего снимка декодера (dB)
|
||
end;
|
||
|
||
function TRadioController.BeaconLockEnabled: Boolean;
|
||
begin
|
||
Result := FBeaconLockOn;
|
||
end;
|
||
|
||
function TRadioController.BeaconRefHz: Double;
|
||
begin
|
||
Result := FBeaconRefHz;
|
||
end;
|
||
|
||
function TRadioController.BeaconTrackedFreqHz: Double;
|
||
begin
|
||
Result := FBeaconTrackHz;
|
||
end;
|
||
|
||
function TRadioController.BeaconDecodeFreqHz: Double;
|
||
begin
|
||
Result := FBeaconDecHz;
|
||
end;
|
||
|
||
procedure TRadioController.SetBeaconDecodeInvert(On_: Boolean);
|
||
begin
|
||
FBeaconDecInvert := On_;
|
||
if Assigned(FBeaconDec) then FBeaconDec.SetInvert(On_);
|
||
Changed(rfBeaconLock);
|
||
end;
|
||
|
||
function TRadioController.BeaconDecodeInvert: Boolean;
|
||
begin
|
||
Result := FBeaconDecInvert;
|
||
end;
|
||
|
||
function TRadioController.GetBeaconScope(out S: TBeaconScope): Boolean;
|
||
begin
|
||
Result := Assigned(FBeaconDec);
|
||
if Result then FBeaconDec.GetScope(S)
|
||
else FillChar(S, SizeOf(S), 0);
|
||
end;
|
||
|
||
function TRadioController.GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
|
||
begin
|
||
Result := Assigned(FBeaconDec) and FBeaconDec.GetLastFrame(Frame, RSErrors);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Лупа маяка: окно наведения высокого разрешения (BeaconScopeForm)
|
||
// ---------------------------------------------------------------------------
|
||
// Движок работает в Гц относительно центра DDC, наружу отдаём абсолютные
|
||
// display-Гц — в них живут BeaconSeedAtHz, маркеры и клики пользователя.
|
||
|
||
procedure TRadioController.GetCaptureWindow(out ALowHz, AHighHz: Double);
|
||
var Half: Double;
|
||
begin
|
||
Half := FSpanHz / 2.0;
|
||
if Half <= 0 then Half := FSampleRate / 2.0;
|
||
ALowHz := FCenterFreq - Half;
|
||
AHighHz := FCenterFreq + Half;
|
||
end;
|
||
|
||
procedure TRadioController.SetBeaconZoomWindow(AEnabled: Boolean;
|
||
ACenterHz, ASpanHz: Double);
|
||
begin
|
||
if not Assigned(FDSPEngine) then Exit;
|
||
if AEnabled and (ACenterHz > 0) then
|
||
FDSPEngine.SetBeaconZoom(True, ACenterHz - FCenterFreq, ASpanHz)
|
||
else
|
||
FDSPEngine.SetBeaconZoom(False, 0, 0);
|
||
end;
|
||
|
||
function TRadioController.GetBeaconZoomSpectrum(var Pixels: array of Single;
|
||
out Count: Integer; out LowHz, HighHz: Double): Boolean;
|
||
begin
|
||
Count := 0;
|
||
LowHz := 0;
|
||
HighHz := 0;
|
||
Result := False;
|
||
if not Assigned(FDSPEngine) then Exit;
|
||
Result := FDSPEngine.GetBeaconZoomSpectrum(Pixels, Count, LowHz, HighHz);
|
||
if Count > 0 then
|
||
begin
|
||
LowHz := LowHz + FCenterFreq;
|
||
HighHz := HighHz + FCenterFreq;
|
||
end;
|
||
end;
|
||
|
||
function TRadioController.GetBeaconZoomWaterfall(var Pixels: array of Single;
|
||
out Count: Integer): Boolean;
|
||
begin
|
||
Count := 0;
|
||
Result := False;
|
||
if not Assigned(FDSPEngine) then Exit;
|
||
Result := FDSPEngine.GetBeaconZoomWaterfall(Pixels, Count);
|
||
end;
|
||
|
||
function TRadioController.BeaconZoomBinHz: Double;
|
||
begin
|
||
Result := 0;
|
||
if not Assigned(FDSPEngine) then Exit;
|
||
if FDSPEngine.BeaconZoomFFT > 0 then
|
||
Result := FDSPEngine.SampleRate / FDSPEngine.BeaconZoomFFT;
|
||
end;
|
||
|
||
function TRadioController.GetDMRStatus(out S: TDMRStatus): Boolean;
|
||
begin
|
||
Result := Assigned(FDMRDec);
|
||
if Result then FDMRDec.GetStatus(S)
|
||
else FillChar(S, SizeOf(S), 0);
|
||
end;
|
||
|
||
function TRadioController.GetSliceDMRStatus(SliceId: Integer;
|
||
out S: TDMRStatus): Boolean;
|
||
var
|
||
Idx: Integer;
|
||
begin
|
||
Idx := FindSliceIndex(SliceId);
|
||
Result := (Idx >= 0) and (FSlices[Idx].Mode = MODE_DMR) and
|
||
Assigned(FSlices[Idx].DMR);
|
||
if Result then FSlices[Idx].DMR.GetStatus(S)
|
||
else FillChar(S, SizeOf(S), 0);
|
||
end;
|
||
|
||
// Кромки полосы под режим/полосу (единый расчёт для главного канала и слайсов).
|
||
class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
||
// Обёртка над единственной таблицей знака (Settings.FilterEdgesFromBW) —
|
||
// оставлена ради прежних вызывающих (MainForm, SetSliceModeBW).
|
||
begin
|
||
FilterEdgesFromBW(Mode, BW, Lo, Hi);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceModeBW(Id, Mode, BW: Integer);
|
||
var Lo, Hi: Integer;
|
||
begin
|
||
// DMR has one fixed RF channel width; callers select only the mode.
|
||
if Mode = MODE_DMR then BW := FILT_DMR_BW;
|
||
FilterEdgesFor(Mode, BW, Lo, Hi);
|
||
SetSliceMode(Id, Mode);
|
||
SetSliceFilter(Id, Lo, Hi);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyModeFilter;
|
||
var
|
||
Lo, Hi: Integer;
|
||
begin
|
||
// Кромки берём как есть: они уже знаковые и уже под текущий режим (таблица
|
||
// фильтров ведётся ПО режиму), конвертировать боковую больше нечем и негде.
|
||
Lo := FFilterLo;
|
||
Hi := FFilterHi;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
// Только RX. TX-полоса НЕ следует за RX-фильтром (модель Thetis) — её
|
||
// единственный источник TX-фильтр из настроек (SetTXFilterFull/ApplyTXChain
|
||
// через TXSignedEdges). Иначе широкий RX раздувал бы излучаемый сигнал.
|
||
FDSPEngine.SetFilter(Lo, Hi);
|
||
if FMode in [MODE_FM, MODE_DMR, MODE_FMRAW] then
|
||
begin
|
||
if FMode = MODE_FMRAW then
|
||
FDSPEngine.SetRawFMDeviation(FRawFMDeviation)
|
||
else
|
||
FDSPEngine.SetRXFMDeviation(FFMDeviation);
|
||
if FMode <> MODE_FMRAW then
|
||
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut,
|
||
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
|
||
// A squelch would remove sync preambles; DMR always receives open.
|
||
FDSPEngine.SetFMSquelch((FMode = MODE_FM) and FFMSQOn, FFMSQLevel);
|
||
end
|
||
else
|
||
FDSPEngine.SetFMSquelch(False, 0);
|
||
end;
|
||
end;
|
||
|
||
// Выбирает дефолтный фильтр (индекс + полоса + FM-девиация) под текущий FMode.
|
||
// Зеркалит state-часть UI UpdateFilterButtons, чтобы смена режима была
|
||
// корректной и без UI (headless). FM сохраняет текущий NFM/WFM, прочие — дефолт.
|
||
function TRadioController.ClampFilterIdx(Mode, Idx: Integer): Integer;
|
||
// VAR1/VAR2 есть у любого режима, поэтому проверяем только пресетную часть.
|
||
// Выход за таблицу режима возвращает ЗАВОДСКОЙ индекс (а не крайний) — так
|
||
// вело себя прежнее «if FFilter > FILT_WFM_COUNT-1 then FILT_WFM_DEF».
|
||
begin
|
||
Result := Idx;
|
||
if (Idx = FILT_VAR1) or (Idx = FILT_VAR2) then Exit;
|
||
if (Idx < 0) or (Idx > FilterPresetCount(Mode) - 1) then
|
||
Result := FilterDefIdx(Mode);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyFilterSlot(Idx: Integer);
|
||
var S: TFilterSlot;
|
||
begin
|
||
FFilter := ClampFilterIdx(FMode, Idx);
|
||
S := FFilterTable[FMode][FFilter];
|
||
FFilterLo := S.Lo;
|
||
FFilterHi := S.Hi;
|
||
FFilterBW := FilterBWFromEdges(FMode, S.Lo, S.Hi);
|
||
// FM-девиация привязана к пресету (NFM/WFM), у VAR берём девиацию дефолта.
|
||
if FMode = MODE_FM then
|
||
begin
|
||
if FFilter <= 1 then FFMDeviation := FILT_FM_DEV[FFilter]
|
||
else FFMDeviation := FILT_FM_DEV[FILT_FM_DEF];
|
||
end
|
||
else if FMode = MODE_DMR then
|
||
FFMDeviation := DMR_FM_DEV;
|
||
end;
|
||
|
||
procedure TRadioController.ApplyModeDefaults;
|
||
begin
|
||
// FM/WFM/DMR/FMRAW сохраняют выбранный пресет (NFM↔WFM и т.п.), остальные
|
||
// режимы возвращаются к заводскому индексу — как было до таблицы фильтров.
|
||
case FMode of
|
||
MODE_FM, MODE_WFM, MODE_DMR, MODE_FMRAW: ApplyFilterSlot(FFilter);
|
||
else ApplyFilterSlot(FilterDefIdx(FMode));
|
||
end;
|
||
end;
|
||
|
||
function TRadioController.FilterBWFor(M, Idx: Integer): Integer;
|
||
// Полоса выбранного фильтра под режим. Зеркалит выбор таблицы в UpdateFilterButtons,
|
||
// но НЕ меняет индекс (в отличие от ApplyModeDefaults) — нужно для XVTR-restore.
|
||
var S: TFilterSlot;
|
||
begin
|
||
M := EnsureRange(M, 0, CFG_MODE_MAX);
|
||
S := FFilterTable[M][ClampFilterIdx(M, Idx)];
|
||
Result := FilterBWFromEdges(M, S.Lo, S.Hi);
|
||
end;
|
||
|
||
function TRadioController.FilterIdxFor(M, BW: Integer): Integer;
|
||
// Обратно к FilterBWFor: индекс пресета с минимальным отклонением от BW.
|
||
var
|
||
i, Cnt, D, Best: Integer;
|
||
begin
|
||
Result := 0;
|
||
case M of
|
||
MODE_FM: Cnt := FILT_FM_COUNT;
|
||
MODE_WFM: Cnt := FILT_WFM_COUNT;
|
||
MODE_FMRAW: Cnt := FILT_RAW_COUNT;
|
||
MODE_DMR: Exit;
|
||
else Cnt := FILT_COUNT;
|
||
end;
|
||
Best := MaxInt;
|
||
for i := 0 to Cnt - 1 do
|
||
begin
|
||
D := Abs(FilterBWFor(M, i) - BW);
|
||
if D < Best then begin Best := D; Result := i; 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 (TxBandIdx >= 0) and (TxBandIdx < CFG_BAND_COUNT) then
|
||
Cal := FPABandCal[TxBandIdx] // банд TX-слайса при кросс-банд TX
|
||
else
|
||
Cal := 38.8;
|
||
|
||
if FTuning then
|
||
begin
|
||
// Уровень настройки всегда идёт от глобального TUN Level — иначе ручка в
|
||
// Transmit в трансвертере оказывалась мёртвой. Мощность слота (если
|
||
// включена галка) работает МНОЖИТЕЛЕМ, ровно как для обычного drive ниже:
|
||
// это «сколько мощности трансиверa разрешено этому трансвертеру», и к
|
||
// настройке оно относится так же, как к передаче.
|
||
Pos := EnsureRange(FTXSettings.TUNLevel, 0, 100);
|
||
if XvtrActive and FXvtrSettings.UseXVTRTunePower then
|
||
Pos := Round(Pos * EnsureRange(FXvtrSettings.Entries[FCurrentXvtr].TXPower,
|
||
0, 100) / 100.0);
|
||
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;
|
||
|
||
procedure TRadioController.SanitizeFilterForMode;
|
||
// У FM/DMR/FMRAW пресетов меньше, чем у SSB: приводим индекс к допустимому и
|
||
// синхронизируем кромки/BW/девиацию. Чистая (state) часть UpdateFilterButtons.
|
||
begin
|
||
if FMode in [MODE_FM, MODE_DMR, MODE_FMRAW] then ApplyFilterSlot(FFilter);
|
||
end;
|
||
|
||
function TRadioController.MakeBandSettings: TBandSettings;
|
||
begin
|
||
Result.VfoA := FVfoA;
|
||
Result.VfoB := FVfoB;
|
||
Result.Mode := FMode;
|
||
Result.FilterIdx := FFilter;
|
||
Result.FilterBW := FFilterBW;
|
||
Result.AGCMode := FAGCMode;
|
||
Result.AGCTop := FAGCTop;
|
||
Result.CTun := FCTun;
|
||
Result.CenterHz := FCenterFreq; // DDC-центр → восстановим offset CTun при загрузке
|
||
Result.SpanHz := FSpanHz;
|
||
Result.ZoomFactor := FZoomFactor;
|
||
Result.PanSlider := FPanSlider;
|
||
Result.FMSQOn := FFMSQOn;
|
||
Result.FMSQLevel := FFMSQLevel;
|
||
Result.CTCSSOn := FFMCTCSSOn;
|
||
Result.CTCSSToneIdx := FFMCTCSSToneIdx;
|
||
Result.FMStepOn := FFMStepOn;
|
||
Result.FMStepIdx := FFMStepIdx;
|
||
Result.FMRptDir := FFMRptDir;
|
||
Result.FMRptOffsetHz := FFMRptOffsetHz;
|
||
// Аппаратный AGC AD936x — на диапазон (усиление тракта разное на 2м и QO-100).
|
||
Result.RxGainMode := FRxGainMode;
|
||
Result.RxGainDb := FRxGainDb;
|
||
Result.AttenDB := FAtten; // шаговый аттенюатор ADC — тоже на диапазон
|
||
// Waterfall AGC/NF — глобальные (не диапазонные)
|
||
end;
|
||
|
||
procedure TRadioController.SaveCurrentBand;
|
||
// Сохраняет текущее состояние диапазона. В XVTR-режиме пишем в XVTR-слот
|
||
// (HF band-cache не трогаем — там сохранён HF-диапазон до активации XVTR).
|
||
var
|
||
i: Integer;
|
||
B: TBandSettings;
|
||
begin
|
||
if not FDevConnected then Exit;
|
||
if FCurrentXvtr >= 0 then
|
||
begin
|
||
if (FCurrentXvtr < CFG_XVTR_COUNT) then
|
||
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;
|
||
FXvtrSettings.Entries[i].LastFMSQLevel := FFMSQLevel;
|
||
FXvtrSettings.Entries[i].LastCTCSSOn := FFMCTCSSOn;
|
||
FXvtrSettings.Entries[i].LastCTCSSToneIdx := FFMCTCSSToneIdx;
|
||
FXvtrSettings.Entries[i].LastFMStepOn := FFMStepOn;
|
||
FXvtrSettings.Entries[i].LastFMStepIdx := FFMStepIdx;
|
||
FXvtrSettings.Entries[i].LastFMRptDir := FFMRptDir;
|
||
FXvtrSettings.Entries[i].LastFMRptOffsetHz := FFMRptOffsetHz;
|
||
FXvtrSettings.Entries[i].LastCTun := FCTun;
|
||
FXvtrSettings.Entries[i].LastCenterHz := FCenterFreq;
|
||
FXvtrSettings.Entries[i].LastAGCMode := FAGCMode;
|
||
FXvtrSettings.Entries[i].LastAGCTop := FAGCTop;
|
||
FXvtrSettings.Entries[i].LastCTCSSAutoActive := FFMCTCSSAutoActive;
|
||
FXvtrSettings.Entries[i].LastFMRptAutoActive := FFMRptAutoActive;
|
||
FXvtrSettings.Entries[i].LastZoomFactor := FZoomFactor;
|
||
FXvtrSettings.Entries[i].LastPanSlider := FPanSlider;
|
||
FXvtrSettings.Entries[i].LastRxGainMode := FRxGainMode;
|
||
FXvtrSettings.Entries[i].LastRxGainDb := FRxGainDb;
|
||
FXvtrSettings.Entries[i].LastAttenDB := FAtten;
|
||
end;
|
||
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||
Exit;
|
||
end;
|
||
// HPSDR: приёмник только HF — если VFO ушёл в VHF/UHF (напр. канал на 2м без
|
||
// XVTR), не сохраняем, иначе HF-кэш диапазона перезапишется VHF-частотой.
|
||
// Pluto: VHF/UHF — нативные диапазоны (VU-план), сохранять обязательно;
|
||
// от перезаписи чужим диапазоном защищает FreqToBandIdx-проверка ниже.
|
||
if (not IsPluto) and (FVfoA > 61000000) then Exit;
|
||
B := MakeBandSettings;
|
||
// Bandstack: VFO A и B независимы и могут стоять на разных бэндах. В кэш
|
||
// ЭТОГО бэнда пишем только ту частоту VFO, что реально в нём — иначе чужая
|
||
// частота (напр. VFO A на 40м при сохранении 30м) загрязнит memory бэнда.
|
||
if FreqToBandIdx(FVfoA, IsPluto) <> FCurrentBand then B.VfoA := FBandCache[FCurrentBand].VfoA;
|
||
if FreqToBandIdx(FVfoB, IsPluto) <> FCurrentBand then B.VfoB := FBandCache[FCurrentBand].VfoB;
|
||
FBandCache[FCurrentBand] := B;
|
||
FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]);
|
||
end;
|
||
|
||
function TRadioController.BuildGlobalSettings: TGlobalSettings;
|
||
// Радио-поля — из живого состояния; тема/FPS/digits/CAT — сквозь из FLoadedGlobal
|
||
// (контроллер их не трактует). Сидируем загруженным блобом, поверх — радио.
|
||
var i: Integer;
|
||
begin
|
||
Result := FLoadedGlobal; // несёт не-радио поля (тема/FPS/digits/CAT) round-trip
|
||
|
||
Result.Volume := FVolume;
|
||
Result.TxMonVolume := FTxMonVolume;
|
||
Result.DriveLevel := FDrivePercent;
|
||
Result.PAMaxPower := FPAMaxPower;
|
||
Result.PlutoTxMaxAttDb := FPlutoTxMaxAttDb;
|
||
for i := 0 to CFG_BAND_COUNT - 1 do Result.PABandCal[i] := FPABandCal[i];
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do Result.VHFBandCal[i] := FVHFBandCal[i];
|
||
Result.Cal := FCal;
|
||
Result.ActiveVfo := FActiveVfo;
|
||
Result.NRMode := FNRMode;
|
||
Result.NBMode := FNBMode;
|
||
Result.SNBEnabled := FSNB;
|
||
Result.ANFEnabled := FANF;
|
||
Result.AGCSlope := 0;
|
||
Result.AGCHangThreshold := 100;
|
||
Result.WfAGCEnabled := FWfAGCEnabled;
|
||
Result.WfNFEnabled := FWfNFEnabled;
|
||
Result.DitherEnabled := FDitherEnabled;
|
||
Result.RandomEnabled := FRandomEnabled;
|
||
Result.LastBand := FCurrentBand;
|
||
Result.LastXvtr := FCurrentXvtr;
|
||
Result.SampleRate := FSampleRate;
|
||
Result.RxGainMode := FRxGainMode;
|
||
Result.RxGainDb := FRxGainDb;
|
||
if FWDSPReady then
|
||
begin
|
||
Result.FFTSize := FDSPEngine.FFTSize;
|
||
Result.WindowType := FDSPEngine.WindowType;
|
||
Result.SpecDetector := FDSPEngine.SpecDetector;
|
||
Result.SpecAvgMode := FDSPEngine.SpecAvgMode;
|
||
Result.SpecAvgTimeMS := FDSPEngine.SpecAvgTimeMS;
|
||
Result.WfDetector := FDSPEngine.WfDetector;
|
||
Result.WfAvgMode := FDSPEngine.WfAvgMode;
|
||
Result.WfAvgTimeMS := FDSPEngine.WfAvgTimeMS;
|
||
end
|
||
else
|
||
begin
|
||
Result.FFTSize := 131072;
|
||
Result.WindowType := 2;
|
||
Result.SpecDetector := 0;
|
||
Result.SpecAvgMode := 3;
|
||
Result.SpecAvgTimeMS := 30.0;
|
||
Result.WfDetector := 0;
|
||
Result.WfAvgMode := 3;
|
||
Result.WfAvgTimeMS := 120.0;
|
||
end;
|
||
Result.WfManualHigh := FWfManualHigh;
|
||
Result.WfManualLow := FWfManualLow;
|
||
Result.WfAGCOffset := FWfAGCOffset;
|
||
Result.WfPalette := FWfPalette;
|
||
Result.WfGamma := FWfGamma;
|
||
Result.DisplayPixels := FDisplayPixels;
|
||
Result.WfSpeedDiv := FWfSpeedDiv;
|
||
Result.SpecMSAA := FSpecMSAA;
|
||
Result.SpecRefLevel := FSpecRefLevel;
|
||
Result.SpecRange := FSpecRange;
|
||
Result.SpecGridStep := FSpecGridStep;
|
||
Result.BeaconZoomSpanHz := FBcnZoomSpanHz;
|
||
Result.BeaconZoomOffsetHz := FBcnZoomOffsetHz;
|
||
Result.BeaconZoomFollow := FBcnZoomFollow;
|
||
Result.BeaconZoomAuto := FBcnZoomAuto;
|
||
Result.AudioSampleRate := FAudioOut.SampleRate;
|
||
Result.ShowSpectrum := FShowSpectrum;
|
||
Result.ShowWaterfall := FShowWaterfall;
|
||
Result.ShowWideband := FShowWideband;
|
||
Result.WidebandFill := FWidebandFill;
|
||
Result.SpectrumFill := FSpectrumFill;
|
||
Result.DisplayDuplex := FDisplayDuplex;
|
||
Result.RxMuteOnTx := FRxMuteOnTx;
|
||
Result.AudioOutDevice := FAudioOutDevName;
|
||
Result.AudioInDevice := FAudioInDevName;
|
||
Result.SendAudioToRadio := FSendAudioToRadio;
|
||
Result.RawFMDeviation := Round(FRawFMDeviation);
|
||
end;
|
||
|
||
procedure TRadioController.SaveGlobalSettings;
|
||
begin
|
||
if FDevConnected then FSettings.SaveGlobal(FDevMAC, BuildGlobalSettings);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyBandDSP(const B: TBandSettings);
|
||
// DSP-настройки диапазона + события рендера. Частоты VFO/центр НЕ трогает.
|
||
begin
|
||
// --- Режим ---
|
||
FMode := EnsureRange(B.Mode, MODE_MIN, MODE_MAX);
|
||
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
|
||
|
||
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
|
||
// Банд помнит ТОЛЬКО выбранный слот; кромки живут в таблице фильтров
|
||
// (по режиму, как в Thetis). Иначе у VAR было бы два источника истины —
|
||
// ровно та болезнь, от которой уходили.
|
||
ApplyFilterSlot(B.FilterIdx);
|
||
ApplyModeFilter;
|
||
|
||
// --- AGC (единый маппинг — исправляет каст-баг RestoreBand) ---
|
||
FAGCMode := B.AGCMode;
|
||
FAGCTop := B.AGCTop;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetAGCTop(FAGCTop);
|
||
ApplyAGCToEngine;
|
||
|
||
// --- Аппаратный AGC AD936x (Pluto/LibreSDR): режим + manual-гейн диапазона.
|
||
// У openHPSDR SetRxGain — no-op, поля просто ездят с диапазоном.
|
||
FRxGainMode := EnsureRange(B.RxGainMode, 0, 3);
|
||
FRxGainDb := EnsureRange(B.RxGainDb, 0, 73);
|
||
ApplyRxGainToBackend;
|
||
Changed(rfRxGain); // UI: кнопки FAST/SLOW/HYB/MAN + ползунок
|
||
|
||
// --- Шаговый аттенюатор ADC (openHPSDR) — на диапазон/слот трансвертера.
|
||
FAtten := EnsureRange(B.AttenDB, 0, 31);
|
||
if Assigned(FNetwork) then FNetwork.SetStepAtten(Byte(FAtten));
|
||
Changed(rfAtten);
|
||
|
||
// --- FM squelch / CTCSS / step / repeater ---
|
||
FFMSQOn := B.FMSQOn;
|
||
FFMSQLevel := B.FMSQLevel;
|
||
FFMCTCSSOn := B.CTCSSOn;
|
||
FFMCTCSSToneIdx := B.CTCSSToneIdx;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
|
||
FDSPEngine.SetFMSquelch(FFMSQOn and (FMode = MODE_FM), FFMSQLevel);
|
||
end;
|
||
FFMStepOn := B.FMStepOn;
|
||
FFMStepIdx := EnsureRange(B.FMStepIdx, 0, 3);
|
||
if FMode in [MODE_DMR, MODE_FMRAW] then
|
||
begin
|
||
FFMStepOn := True;
|
||
FFMStepIdx := 1; // DMR default raster: 12.5 kHz
|
||
end;
|
||
FFMRptDir := B.FMRptDir;
|
||
FFMRptOffsetHz := B.FMRptOffsetHz;
|
||
if FFMRptOffsetHz <= 0 then FFMRptOffsetHz := RptDefaultOffsetHz(B.VfoA);
|
||
|
||
// --- TX-профиль под новый режим ---
|
||
// Смена диапазона и вход/выход трансвертера меняют режим ГЛАВНОГО здесь, а
|
||
// не через SetMode, поэтому автоматику привязок надо звать и отсюда — иначе
|
||
// при переходе КВ↔трансвертер оставался профиль прошлого режима.
|
||
if FTxSliceId = 0 then ApplyBoundTXProfile;
|
||
// FDSPEngine.SetMode выше насильно выставил TXA в режим главного приёмника,
|
||
// но передавать можем со слайса — возвращаем режим настоящего источника.
|
||
if FWDSPReady and Assigned(FDSPEngine) and (ActiveTXMode <> FMode) then
|
||
FDSPEngine.SetTXMode(ActiveTXMode);
|
||
|
||
// --- CTUN ---
|
||
FCTun := B.CTun;
|
||
if FWDSPReady and Assigned(FDSPEngine) and not FCTun then FDSPEngine.SetShift(0.0);
|
||
|
||
// --- Span (sample rate глобальный, не per-band — лишь ресинк) ---
|
||
FSpanHz := FSampleRate;
|
||
|
||
// --- Рендер: единичные события (UI рисует через OnControllerState) ---
|
||
Changed(rfBand);
|
||
Changed(rfMode); // rfMode-рендер зовёт UpdateFilterButtons (FM-панели)
|
||
Changed(rfFilter);
|
||
Changed(rfAGCMode);
|
||
Changed(rfAGCTop);
|
||
Changed(rfFMSQ);
|
||
Changed(rfFMSQLevel);
|
||
Changed(rfFMCTCSS);
|
||
Changed(rfFMCTCSSTone);
|
||
Changed(rfFMStep); // кнопка вкл/выкл STEP
|
||
Changed(rfFMStepIdx); // дропдаун шага + web + grid
|
||
Changed(rfFMRpt);
|
||
Changed(rfCTun);
|
||
end;
|
||
|
||
procedure TRadioController.RestoreBand(BandIdx: Integer);
|
||
// Загружает состояние диапазона из кэша, применяет к DSP, делает финальную
|
||
// перестройку приёмника на активный VFO и шлёт события рендера. Полная команда —
|
||
// зовётся напрямую desktop/web/channel/connect-restore.
|
||
var B: TBandSettings;
|
||
begin
|
||
if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit;
|
||
Changed(rfBandRestore); // явная смена диапазона → UI сбросит Wf-avg буфер
|
||
B := FBandCache[BandIdx];
|
||
FCurrentBand := BandIdx;
|
||
|
||
// Pluto: кэш диапазона может нести HF-частоту (дефолты Settings). Если VfoA не
|
||
// попадает в VHF-слот — берём частоту по умолчанию из плана.
|
||
if IsPluto and (FreqToBandIdx(B.VfoA, True) <> BandIdx) then
|
||
begin
|
||
B.VfoA := BandDefaultFreq(BandIdx, True);
|
||
B.VfoB := B.VfoA;
|
||
end;
|
||
|
||
ApplyBandDSP(B);
|
||
|
||
// --- Zoom/pan диапазона: восстанавливаем и переармируем анализатор ---
|
||
FZoomFactor := EnsureRange(B.ZoomFactor, 0.0, 1.0);
|
||
FPanSlider := EnsureRange(B.PanSlider, 0.0, 1.0);
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetZoomPan(FZoomFactor, FPanSlider);
|
||
Changed(rfZoom);
|
||
|
||
// --- VFO / drive (только RestoreBand — bandstack-путь VFO не трогает) ---
|
||
// При CTun восстанавливаем сохранённый DDC-центр, чтобы VFO остался на своём
|
||
// месте в полосе (а не прыгал в центр). Берём только если offset влезает в
|
||
// текущий span — иначе (сменился sample rate / битый конфиг) центрируем на VFO.
|
||
// ApplyTuneCore в SetVfoA при необходимости сам прокрутит центр к краю.
|
||
if FCTun and (B.CenterHz > 0) and (Abs(B.VfoA - B.CenterHz) < FSpanHz / 2) then
|
||
FCenterFreq := B.CenterHz
|
||
else
|
||
FCenterFreq := B.VfoA;
|
||
FVfoB := B.VfoB;
|
||
FVfoA := B.VfoA;
|
||
FDriveLevel := CalcDriveByte;
|
||
|
||
Changed(rfVfoB);
|
||
// Финальная перестройка приёмника на АКТИВНЫЙ VFO. VFO A — через SetVfoA
|
||
// (хук OnAfterTune несёт канальную оркестрацию); VFO B — через SetVfoB.
|
||
// Иначе смена диапазона при активном B центрировала бы приёмник на A.
|
||
if FActiveVfo = 0 then SetVfoA(FVfoA)
|
||
else SetVfoB(FVfoB);
|
||
ApplyAntPorts; // AD936x: разъёмы, выбранные для этого диапазона
|
||
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
|
||
MODE_LSB: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
|
||
MODE_USB: 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, IsPluto);
|
||
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);
|
||
// Канальная оркестрация — ПОСЛЕ рендера rfVfoA (вне его try/finally-гарда), без
|
||
// ре-ентранси: OnVfoTuned зовёт команды (SetDrive/SetFMRpt), которым нужен
|
||
// чистый верхний уровень OnControllerState.
|
||
if Assigned(FOnAfterTune) then FOnAfterTune;
|
||
end;
|
||
|
||
procedure TRadioController.StoreVfoA(Hz: Double);
|
||
// Сохранить частоту VFO A БЕЗ перестройки приёмника (когда активен B — слушаем B,
|
||
// A только хранится/отображается). Симметрично «неактивной» ветке SetVfoB.
|
||
// Рендер FreqDispA — через rfVfoA (спектр он не трогает, т.к. SyncSpecViewFreq
|
||
// держит активный VFO B); смена band-кнопки — через rfBand.
|
||
var BandIdx: Integer;
|
||
begin
|
||
FVfoA := Hz;
|
||
if FCurrentXvtr < 0 then
|
||
begin
|
||
BandIdx := FreqToBandIdx(FVfoA, IsPluto);
|
||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||
begin
|
||
FCurrentBand := BandIdx;
|
||
FDriveLevel := CalcDriveByte;
|
||
Changed(rfBand);
|
||
end;
|
||
end;
|
||
Changed(rfVfoA);
|
||
end;
|
||
|
||
procedure TRadioController.SetVfoB(Hz: Double);
|
||
// VFO B всегда сохраняется. Если B активен — перестраиваем приёмник через
|
||
// ApplyTuneCore (с учётом CTUN), детектим диапазон и толкаем в сеть. Если B
|
||
// неактивен (split, слушаем A) — только сохраняем частоту (для TX).
|
||
// Рендер FreqDispB/спектра — в UI через OnControllerState(rfVfoB).
|
||
var
|
||
BandIdx: Integer;
|
||
NewFreq: Int64;
|
||
begin
|
||
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);
|
||
if FCurrentXvtr < 0 then
|
||
begin
|
||
BandIdx := FreqToBandIdx(FVfoB, IsPluto);
|
||
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 с перестройкой приёмника на его частоту.
|
||
// 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, NeedPush: Boolean;
|
||
NewBand: Integer;
|
||
begin
|
||
FActiveVfo := Idx;
|
||
ActiveFreq := ActiveVfoHz;
|
||
NeedPush := False;
|
||
|
||
// --- Bandstack: активируемый VFO на другом ham-бэнде → восстановить бэнд ---
|
||
if FCurrentXvtr < 0 then
|
||
begin
|
||
NewBand := FreqToBandIdx(ActiveFreq, IsPluto);
|
||
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
|
||
HalfSpan := FSpanHz / 2;
|
||
Offset := ActiveFreq - FCenterFreq;
|
||
FreqVisible := (Offset > -HalfSpan) and (Offset < HalfSpan);
|
||
if FreqVisible then
|
||
begin
|
||
// Частота видна — просто сдвигаем демодулятор.
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset);
|
||
end
|
||
else
|
||
begin
|
||
// Частота за окном — сбрасываем CTUN и центрируемся на ней.
|
||
FCTun := False;
|
||
if XvtrSlotActive then FXvtrSettings.Entries[FCurrentXvtr].LastCTun := False
|
||
else FBandCache[FCurrentBand].CTun := False;
|
||
FCenterFreq := ActiveFreq;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
||
NeedPush := True;
|
||
Changed(rfCTun);
|
||
end;
|
||
end
|
||
else
|
||
begin
|
||
FCenterFreq := ActiveFreq;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
||
NeedPush := True;
|
||
end;
|
||
|
||
RefreshSliceShifts; // центр мог сместиться — держим слайсы на их TargetHz
|
||
if NeedPush then PushNetworkState;
|
||
Changed(rfActiveVfo);
|
||
end;
|
||
|
||
procedure TRadioController.TuneActiveBy(DeltaHz: Double);
|
||
begin
|
||
if FVfoLock then Exit; // VFO заблокирован — перестройку игнорируем
|
||
if FActiveVfo = 0 then SetVfoA(FVfoA + DeltaHz)
|
||
else SetVfoB(FVfoB + DeltaHz);
|
||
end;
|
||
|
||
procedure TRadioController.SetCenter(Hz: Double);
|
||
// CTUN-скролл окна просмотра: двигаем DDC-центр БЕЗ смены VFO. Демодулятор
|
||
// сдвигаем = активный VFO − новый центр; полный HP-кадр в радио. Рендер
|
||
// (ruler/спектр) — в UI через OnControllerState(rfCenterFreq).
|
||
// Точки входа: десктопный CTUN-drag и web (SyncWebCenter).
|
||
begin
|
||
FCenterFreq := Hz;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetShift(ActiveVfoHz - FCenterFreq);
|
||
RefreshSliceShifts; // слайсы привязаны к абсолютной частоте — сдвинуть NCO
|
||
PushNetworkState;
|
||
Changed(rfCenterFreq);
|
||
end;
|
||
|
||
procedure TRadioController.VfoSwap;
|
||
// Меняем VFO A/B местами и перестраиваем приёмник на новую частоту активного
|
||
// VFO (центр=частота, shift 0 — CTUN-оффсет после свопа теряет смысл).
|
||
var
|
||
T: Double;
|
||
ActiveFreq: Double;
|
||
NewBand: Integer;
|
||
begin
|
||
T := FVfoA; FVfoA := FVfoB; FVfoB := T;
|
||
ActiveFreq := ActiveVfoHz;
|
||
// Bandstack: если активный после свопа VFO на другом ham-бэнде — сохраняем
|
||
// покидаемый бэнд и восстанавливаем DSP нового (mode/filter/AGC/CTUN/FM), как
|
||
// SetActiveVfo. Иначе своп на другой диапазон оставлял бы старые mode/фильтр/AGC
|
||
// при подсвеченной кнопке нового бэнда (рассинхрон).
|
||
if FCurrentXvtr < 0 then
|
||
begin
|
||
NewBand := FreqToBandIdx(ActiveFreq, IsPluto);
|
||
if (NewBand >= 0) and (NewBand <> FCurrentBand) then
|
||
begin
|
||
SaveCurrentBand;
|
||
FCurrentBand := NewBand;
|
||
ApplyBandDSP(FBandCache[NewBand]); // несёт Changed(rfBand) + рендер DSP-полей
|
||
FDriveLevel := CalcDriveByte; // PA-калибровка нового бэнда
|
||
end;
|
||
end;
|
||
FCenterFreq := ActiveFreq;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
||
RefreshSliceShifts;
|
||
PushNetworkState;
|
||
Changed(rfVfoA);
|
||
Changed(rfVfoB);
|
||
end;
|
||
|
||
procedure TRadioController.VfoACopyB; // A>B: частота A копируется в B
|
||
begin
|
||
SetVfoB(FVfoA); // сохраняет B:=A, перестраивает если B активен, рендер rfVfoB
|
||
end;
|
||
|
||
procedure TRadioController.VfoBCopyA; // B>A: частота B копируется в A
|
||
begin
|
||
FVfoA := FVfoB;
|
||
if FActiveVfo = 0 then SetVfoA(FVfoA) // A активен — перестраиваем приёмник
|
||
else Changed(rfVfoA); // A неактивен — только рендер дисплея
|
||
end;
|
||
|
||
procedure TRadioController.SetMode(M: Integer);
|
||
begin
|
||
M := EnsureRange(M, MODE_MIN, MODE_MAX);
|
||
FMode := M;
|
||
// DMR channels use a 12.5 kHz raster by default. Keep the ordinary STEP
|
||
// controls enabled so the operator can select another raster afterwards.
|
||
if FMode in [MODE_DMR, MODE_FMRAW] then
|
||
begin
|
||
FFMStepOn := True;
|
||
if FMode = MODE_DMR then FFMStepIdx := 1; // 12.5 kHz
|
||
Changed(rfFMStep);
|
||
Changed(rfFMStepIdx);
|
||
end;
|
||
// Entering receive-only DMR on the selected main TX source immediately
|
||
// releases an existing carrier. SetTune(False) also releases MOX.
|
||
if ActiveTXMode = MODE_DMR then
|
||
begin
|
||
if FTuning then SetTune(False)
|
||
else if FTransmitting then SetMOX(False);
|
||
end;
|
||
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
|
||
ApplyModeDefaults; // дефолтный фильтр/девиация под режим
|
||
if XvtrSlotActive then FXvtrSettings.Entries[FCurrentXvtr].LastMode := FMode
|
||
else FBandCache[FCurrentBand].Mode := FMode;
|
||
// Профиль привязан к паре «флаг + модуляция»: если передаём с главного,
|
||
// новый режим приносит свой профиль. До пуша в WDSP — ApplyTXChainSettings
|
||
// внутри SetTXMode ниже перешьёт цепь уже с новыми значениями.
|
||
if FTxSliceId = 0 then ApplyBoundTXProfile;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetMode(FMode);
|
||
// SetMode configures the main RX/TX pair; a selected slice may still be
|
||
// the actual TX source and therefore owns the transmitter mode.
|
||
FDSPEngine.SetTXMode(ActiveTXMode);
|
||
// A live mode change must also switch away from web/HW microphone when
|
||
// entering FM RAW; normally the modem is connected through PortAudio.
|
||
if FTransmitting and (ActiveTXMode = MODE_FMRAW) then
|
||
begin
|
||
FWebMicActive := False;
|
||
FTCIMicActive := False;
|
||
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||
if Assigned(ActiveMicInput) and ActiveMicInput.IsOpen then
|
||
ActiveMicInput.Flush;
|
||
end;
|
||
// Если TUN активен — пересчитываем знак частоты под новый режим
|
||
if FTuning then
|
||
begin
|
||
if ActiveTXMode in [MODE_CWL, MODE_CWU] then
|
||
FDSPEngine.SetTXTone(True, FCWSettings.Pitch, TUN_TONE_MAG)
|
||
else
|
||
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG);
|
||
end;
|
||
end;
|
||
SyncCWKeyer;
|
||
ApplyModeFilter;
|
||
Changed(rfMode);
|
||
end;
|
||
|
||
procedure TRadioController.SetFilterIdx(Idx: Integer);
|
||
begin
|
||
// Публичная команда: Idx может прийти из CAT/web без проверки. Клампится
|
||
// внутри ApplyFilterSlot (у FM/WFM/FMRAW пресетов меньше; VAR всегда есть).
|
||
ApplyFilterSlot(Idx);
|
||
CacheFilterToBand;
|
||
ApplyModeFilter;
|
||
Changed(rfFilter);
|
||
end;
|
||
|
||
procedure TRadioController.CacheFilterToBand;
|
||
begin
|
||
if XvtrSlotActive then
|
||
begin
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastFilterIdx := FFilter;
|
||
Exit;
|
||
end;
|
||
FBandCache[FCurrentBand].FilterIdx := FFilter;
|
||
FBandCache[FCurrentBand].FilterBW := FFilterBW; // легаси-поле, для старых конфигов
|
||
end;
|
||
|
||
procedure TRadioController.SelectFilterSlotState(Idx: Integer);
|
||
begin
|
||
ApplyFilterSlot(Idx);
|
||
end;
|
||
|
||
procedure TRadioController.SanitizeFilterState;
|
||
begin
|
||
SanitizeFilterForMode;
|
||
end;
|
||
|
||
procedure TRadioController.TXFilterEdges(out Lo, Hi: Integer);
|
||
begin
|
||
Lo := 0; Hi := 0;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.TXFilterEdgesHz(Lo, Hi);
|
||
end;
|
||
|
||
function TRadioController.FilterSlot(Mode, Idx: Integer): TFilterSlot;
|
||
begin
|
||
Result := FFilterTable[EnsureRange(Mode, 0, CFG_MODE_MAX)]
|
||
[EnsureRange(Idx, 0, FILT_SLOTS - 1)];
|
||
end;
|
||
|
||
procedure TRadioController.SetFilterSlot(Mode, Idx: Integer; const S: TFilterSlot);
|
||
begin
|
||
Mode := EnsureRange(Mode, 0, CFG_MODE_MAX);
|
||
Idx := EnsureRange(Idx, 0, FILT_SLOTS - 1);
|
||
if S.Hi <= S.Lo then Exit; // пустая полоса = глухой приём
|
||
FFilterTable[Mode][Idx] := S;
|
||
if Assigned(FSettings) then FSettings.SaveFilters(FDevMAC, FFilterTable);
|
||
// Правка активного слота слышна сразу — форма настроек в EWSDR мгновенная.
|
||
if (Mode = FMode) and (Idx = FFilter) then
|
||
begin
|
||
ApplyFilterSlot(Idx);
|
||
CacheFilterToBand;
|
||
ApplyModeFilter;
|
||
Changed(rfFilter);
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.ResetFilterSlot(Mode, Idx: Integer);
|
||
var Def: TFilterSet;
|
||
begin
|
||
Mode := EnsureRange(Mode, 0, CFG_MODE_MAX);
|
||
Def := DefaultFilterSet(Mode);
|
||
SetFilterSlot(Mode, Idx, Def[EnsureRange(Idx, 0, FILT_SLOTS - 1)]);
|
||
end;
|
||
|
||
procedure TRadioController.ResetFilterSet(Mode: Integer);
|
||
var i: Integer;
|
||
begin
|
||
for i := 0 to FILT_SLOTS - 1 do ResetFilterSlot(Mode, i);
|
||
end;
|
||
|
||
procedure TRadioController.SetFilterEdges(LoHz, HiHz: Integer);
|
||
// Drag на спектре / CAT ZZFL-ZZFH / web: пресеты не портим — уходим в VAR1.
|
||
var S: TFilterSlot; Target: Integer;
|
||
begin
|
||
if HiHz - LoHz < FILT_MIN_WIDTH then Exit;
|
||
if (FFilter = FILT_VAR1) or (FFilter = FILT_VAR2) then Target := FFilter
|
||
else Target := FILT_VAR1;
|
||
S := FFilterTable[FMode][Target];
|
||
S.Lo := LoHz;
|
||
S.Hi := HiHz;
|
||
FFilterTable[FMode][Target] := S;
|
||
if Assigned(FSettings) then FSettings.SaveFilters(FDevMAC, FFilterTable);
|
||
ApplyFilterSlot(Target);
|
||
CacheFilterToBand;
|
||
ApplyModeFilter;
|
||
Changed(rfFilter);
|
||
end;
|
||
|
||
procedure TRadioController.SetFilterBW(BWHz: Integer);
|
||
// Непрерывная полоса (web-ползунок). Точное совпадение с пресетом выбирает
|
||
// пресет, произвольная ширина уходит в VAR — как и перетаскивание края мышью.
|
||
var
|
||
Lo, Hi, i, n: Integer;
|
||
begin
|
||
if FMode = MODE_DMR then BWHz := FILT_DMR_BW;
|
||
FilterEdgesFromBW(FMode, BWHz, Lo, Hi);
|
||
n := FilterPresetCount(FMode);
|
||
for i := 0 to n - 1 do
|
||
if (FFilterTable[FMode][i].Lo = Lo) and (FFilterTable[FMode][i].Hi = Hi) then
|
||
begin
|
||
SetFilterIdx(i);
|
||
Exit;
|
||
end;
|
||
SetFilterEdges(Lo, Hi);
|
||
end;
|
||
|
||
procedure TRadioController.SetRawFMDeviation(Hz: Integer);
|
||
begin
|
||
FRawFMDeviation := EnsureRange(Hz, RAW_FM_DEV_MIN, RAW_FM_DEV_MAX);
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetRawFMDeviation(FRawFMDeviation);
|
||
SaveGlobalSettings;
|
||
Changed(rfFMDeviation);
|
||
end;
|
||
|
||
// UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для всех
|
||
// фронтендов и слайсов (раньше web/слайдер ошибочно кастовали индекс прямо в enum).
|
||
class function TRadioController.AGCModeFromUI(M: Integer): TWDSPAGCMode;
|
||
const
|
||
AGCModes: array[0..4] of TWDSPAGCMode = (agcFast, agcMedium, agcSlow, agcLong, agcOff);
|
||
begin
|
||
if (M < 0) or (M > 4) then M := 1;
|
||
Result := AGCModes[M];
|
||
end;
|
||
|
||
class function TRadioController.AGCModeToUI(A: TWDSPAGCMode): Integer;
|
||
// Обратно к AGCModeFromUI: 0=FAST,1=MED,2=SLOW,3=LONG,4=OFF (шкала UI и CAT).
|
||
begin
|
||
case A of
|
||
agcFast: Result := 0;
|
||
agcSlow: Result := 2;
|
||
agcLong: Result := 3;
|
||
agcOff: Result := 4;
|
||
else Result := 1; // agcMedium
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.ApplyAGCToEngine;
|
||
begin
|
||
if FWDSPReady and Assigned(FDSPEngine) and (FAGCMode >= 0) and (FAGCMode <= 4) then
|
||
FDSPEngine.SetAGC(AGCModeFromUI(FAGCMode), 50.0);
|
||
end;
|
||
|
||
procedure TRadioController.SetAGCMode(M: Integer);
|
||
begin
|
||
if (M < 0) or (M > 4) then Exit;
|
||
FAGCMode := M;
|
||
ApplyAGCToEngine;
|
||
if XvtrSlotActive then FXvtrSettings.Entries[FCurrentXvtr].LastAGCMode := FAGCMode
|
||
else 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; // ре-применяем режим с новым уровнем
|
||
if XvtrSlotActive then FXvtrSettings.Entries[FCurrentXvtr].LastAGCTop := FAGCTop
|
||
else FBandCache[FCurrentBand].AGCTop := FAGCTop;
|
||
Changed(rfAGCTop);
|
||
end;
|
||
|
||
procedure TRadioController.AGCTopBy(Delta: Integer);
|
||
begin SetAGCTop(FAGCTop + Delta); end;
|
||
|
||
procedure TRadioController.ApplyRxGainToBackend;
|
||
begin
|
||
if Assigned(FNetwork) and FNetwork.Connected then
|
||
FNetwork.SetRxGain(FRxGainMode, FRxGainDb);
|
||
end;
|
||
|
||
procedure TRadioController.SetRxGainMode(M: Integer);
|
||
begin
|
||
if M < 0 then M := 0 else if M > 3 then M := 3;
|
||
FRxGainMode := M;
|
||
ApplyRxGainToBackend;
|
||
Changed(rfRxGain);
|
||
end;
|
||
|
||
procedure TRadioController.SetRxGain(Db: Integer);
|
||
begin
|
||
if Db < 0 then Db := 0 else if Db > 73 then Db := 73;
|
||
FRxGainDb := Db;
|
||
// Ручная установка усиления подразумевает manual-режим (0).
|
||
FRxGainMode := 0;
|
||
ApplyRxGainToBackend;
|
||
Changed(rfRxGain);
|
||
end;
|
||
|
||
procedure TRadioController.RxGainBy(Delta: Integer);
|
||
begin SetRxGain(FRxGainDb + Delta); end;
|
||
|
||
function TRadioController.MonitoringTX: Boolean;
|
||
begin
|
||
// Звук даунлинка на передаче идёт только в full-duplex (DUP) при выключенном
|
||
// RX MUTE — тот же гейт, что в движке (FKeepRXDuringTX and FMonitorRXDuringTX).
|
||
Result := FTransmitting and FDisplayDuplex and (not FRxMuteOnTx);
|
||
end;
|
||
|
||
function TRadioController.ActiveVolume: Integer;
|
||
begin
|
||
if MonitoringTX then Result := FTxMonVolume else Result := FVolume;
|
||
end;
|
||
|
||
procedure TRadioController.ApplyActiveVolume;
|
||
begin
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetVolume(ActiveVolume / 100.0);
|
||
Changed(rfVolume); // слайдер/оверлей ← ActiveVolume
|
||
end;
|
||
|
||
procedure TRadioController.SetVolume(V: Integer);
|
||
begin
|
||
if V < 0 then V := 0 else if V > 100 then V := 100;
|
||
// Правим ту громкость, что сейчас активна: на TX-мониторе — FTxMonVolume,
|
||
// иначе FVolume. Обе персистятся; слайдер редактирует активную.
|
||
if MonitoringTX then FTxMonVolume := V else FVolume := V;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetVolume(ActiveVolume / 100.0);
|
||
// rfVolume — для слайдера и оверлея: они показывают ActiveVolume. А вот кто
|
||
// именно изменился, слайдеру всё равно, зато не всё равно внешним клиентам:
|
||
// у них громкость приёма и громкость монитора — РАЗНЫЕ величины.
|
||
if MonitoringTX then Changed(rfMonVolume);
|
||
Changed(rfVolume);
|
||
end;
|
||
|
||
procedure TRadioController.SetRxVolume(V: Integer);
|
||
begin
|
||
FVolume := EnsureRange(V, 0, 100);
|
||
if MonitoringTX then Changed(rfVolume) // звучит монитор — трогать тракт нечем
|
||
else ApplyActiveVolume; // он же несёт Changed(rfVolume)
|
||
end;
|
||
|
||
procedure TRadioController.VolumeBy(Delta: Integer);
|
||
begin SetVolume(ActiveVolume + Delta); end;
|
||
|
||
procedure TRadioController.SetTXMonVolume(V: Integer);
|
||
// Адресно правит громкость self-monitor'а. Слайдер добирается до неё только на
|
||
// передаче (SetVolume через ActiveVolume), CAT-клиенту такой контекст не нужен.
|
||
begin
|
||
FTxMonVolume := EnsureRange(V, 0, 100);
|
||
if MonitoringTX then ApplyActiveVolume;
|
||
Changed(rfMonVolume);
|
||
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
|
||
if V < 0 then V := 0 else if V > 100 then V := 100;
|
||
FDrivePercent := V;
|
||
// Мощность входит в профиль: ведём её в активном без записи на диск (слайдер
|
||
// шлёт события пачками; на диск профили уходят на переключении/остановке).
|
||
if (FTXProfiles.ActiveIdx >= 0) and (FTXProfiles.ActiveIdx < FTXProfiles.Count) then
|
||
FTXProfiles.Items[FTXProfiles.ActiveIdx].DrivePercent := FDrivePercent;
|
||
FDriveLevel := CalcDriveByte;
|
||
ApplyWDSPDrive(FDrivePercent);
|
||
ApplyPlutoTxAtten; // Pluto: drive живьём меняет TX-аттенюацию во время передачи
|
||
PushNetworkState; // протолкнуть новый drive в радио (если подключено+Running)
|
||
Changed(rfDrive);
|
||
end;
|
||
|
||
procedure TRadioController.DriveBy(Delta: Integer);
|
||
begin SetDrive(FDrivePercent + Delta); end;
|
||
|
||
function TRadioController.PlutoTxAttForDrive: Double;
|
||
const
|
||
TX_OFF = -89.75; // макс. аттенюация AD9361 (0% → практически выкл)
|
||
var
|
||
Pct: Integer;
|
||
begin
|
||
// На тюне (TUN) уровень берём из TX-настроек (TUNLevel), иначе из drive%.
|
||
if FTuning then Pct := FTXSettings.TUNLevel else Pct := FDrivePercent;
|
||
if Pct < 0 then Pct := 0 else if Pct > 100 then Pct := 100;
|
||
if Pct <= 0 then begin Result := TX_OFF; Exit; end; // 0% → TX практически выкл
|
||
// power-linear: мощность ∝ drive%. dB = потолок + 20·log10(pct/100); 100% → потолок,
|
||
// 50% → −6 дБ, 10% → −20 дБ. (Раньше dB-линейно от −89.75 — низкие % улетали в −80 дБ.)
|
||
Result := FPlutoTxMaxAttDb + 20.0 * Log10(Pct / 100.0);
|
||
if Result > FPlutoTxMaxAttDb then Result := FPlutoTxMaxAttDb;
|
||
if Result < TX_OFF then Result := TX_OFF;
|
||
end;
|
||
|
||
procedure TRadioController.ApplyPlutoTxAtten;
|
||
const
|
||
TX_OFF = -89.75;
|
||
begin
|
||
if (FNetwork = nil) or not IsPluto then Exit;
|
||
// На передаче (MOX/TUN) — мощность от drive; на приёме — макс. аттенюация (выкл).
|
||
// ★В телеграфе FTransmitting не поднимается вовсе (передачей владеет кейер),
|
||
// поэтому спрашиваем и сессию локального генератора — иначе манипуляция шла бы
|
||
// в закрытый аттенюатор, то есть в никуда.
|
||
if FTransmitting or FTuning or FCWLocalTX then
|
||
FNetwork.SetTxAtten(PlutoTxAttForDrive)
|
||
else
|
||
FNetwork.SetTxAtten(TX_OFF);
|
||
end;
|
||
|
||
procedure TRadioController.SetPlutoTxMaxAtt(Db: Double);
|
||
begin
|
||
if Db > 0 then Db := 0 else if Db < -50 then Db := -50;
|
||
FPlutoTxMaxAttDb := Db;
|
||
ApplyPlutoTxAtten; // применить живьём, если сейчас передаём
|
||
SaveGlobalSettings; // персист потолка (per-device)
|
||
end;
|
||
|
||
procedure TRadioController.SetCalibration(const C: TCalibration);
|
||
begin
|
||
FCal := C;
|
||
// Санитизация: битый конфиг не должен дать деление на ноль/абсурдные ватты.
|
||
FCal.PwrDetModel := EnsureRange(FCal.PwrDetModel, 0, 3);
|
||
if FCal.AmpSensMVA <= 0 then FCal.AmpSensMVA := 120.0;
|
||
FCal.FreqCalPPM := EnsureRange(FCal.FreqCalPPM, -100.0, 100.0);
|
||
// Опорник: у HPSDR правим частотное слово, у AD936x — xo_correction.
|
||
// Ручные LOError/TO Error трансвертеров остаются сверху.
|
||
if Assigned(FNetwork) then FNetwork.SetFreqCalPPM(FCal.FreqCalPPM);
|
||
// Офсет S-метра берётся из FCal на каждом чтении (SMeterCalFor) — применять
|
||
// отдельно нечего: он зависит от диапазона/трансвертера, а не от движка.
|
||
SaveGlobalSettings; // персист (гейт по FDevConnected внутри)
|
||
end;
|
||
|
||
procedure TRadioController.ApplyWDSPDrive(Pct: Integer);
|
||
// На Pluto мощность задаётся ОДИН раз — железной аттенюацией (PlutoTxAttForDrive),
|
||
// поэтому цифровой уровень WDSP держим на максимуме (1.0), а не режем по drive%.
|
||
// HPSDR — как раньше, Pct/100 (реальная мощность там — drive-байт в радио).
|
||
begin
|
||
if not (FWDSPReady and Assigned(FDSPEngine)) then Exit;
|
||
if IsPluto then FDSPEngine.SetDriveLevel(1.0)
|
||
else FDSPEngine.SetDriveLevel(EnsureRange(Pct, 0, 100) / 100.0);
|
||
end;
|
||
|
||
procedure TRadioController.SetAtten(Db: Integer);
|
||
// Шаговый аттенюатор ADC — плавный 0..31 дБ (как в железе openHPSDR), а не
|
||
// три фиксированные ступени. Уровни S-метра/спектра компенсируются на ту же
|
||
// величину (CalLookup), поэтому шкала dBm остаётся абсолютной.
|
||
begin
|
||
FAtten := EnsureRange(Db, 0, 31);
|
||
if Assigned(FNetwork) then FNetwork.SetStepAtten(Byte(FAtten));
|
||
Changed(rfAtten);
|
||
// Персист — вместе с диапазоном/слотом (SaveCurrentBand на выходе из них),
|
||
// писать JSON на каждый шаг ползунка незачем, как и у drive.
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastAttenDB := FAtten
|
||
else if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
|
||
FBandCache[FCurrentBand].AttenDB := FAtten;
|
||
end;
|
||
|
||
procedure TRadioController.SetBand(Idx: Integer);
|
||
// Смена HF-диапазона по индексу (web/CAT/BandUp-Down). Выход из XVTR (если активен),
|
||
// персист покидаемого, restore нового. Полная команда — не трогает UI.
|
||
begin
|
||
if (Idx < 0) or (Idx >= CFG_BAND_COUNT) then Exit;
|
||
// Пустой слот плана (HF: 12-й, зарезервирован под 12-местный план Pluto) —
|
||
// не диапазон, игнорируем (защита CAT/web/BandUp).
|
||
if BandName(Idx, IsPluto) = '' then Exit;
|
||
if FCurrentXvtr >= 0 then
|
||
begin
|
||
DeactivateXvtr; // выход в HF (HF-возврат внутри)
|
||
if Idx = FCurrentBand then Exit;
|
||
end
|
||
else if Idx = FCurrentBand then Exit;
|
||
if FDevConnected then begin SaveCurrentBand; FSettings.Save; end;
|
||
FCurrentBand := Idx;
|
||
RestoreBand(Idx);
|
||
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.ApplyXvtrToNetwork;
|
||
// Передаёт XVTR-режим в сеть (enable bit + T/R relay suppression + RX ant).
|
||
var
|
||
En, DisablePA: Boolean;
|
||
RxAnt: Byte;
|
||
begin
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
|
||
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
|
||
begin
|
||
En := True;
|
||
DisablePA := FXvtrSettings.Entries[FCurrentXvtr].DisablePA;
|
||
RxAnt := FXvtrSettings.Entries[FCurrentXvtr].RXAntenna;
|
||
end
|
||
else
|
||
begin
|
||
En := False; DisablePA := False; RxAnt := 0;
|
||
end;
|
||
if Assigned(FNetwork) and FNetwork.Connected then
|
||
if En then FNetwork.SetXvtrMode(En, DisablePA, RxAnt, FCurrentXvtr)
|
||
else FNetwork.SetXvtrMode(En, DisablePA, RxAnt, -1);
|
||
end;
|
||
|
||
procedure TRadioController.ActivateXvtr(Idx: Integer);
|
||
// Вход в XVTR-слот: state+DSP через общий ApplyBandDSP + VFO/центр/сеть.
|
||
// Рендер: rfXvtr (XVTR-кнопки), rfBand (XVTR-aware гасит HF), rfActiveVfo
|
||
// (дисплеи A/B + полный перерисов спектра). UI-обёртка добавляет wideband+web.
|
||
var
|
||
E: TXvtrEntry;
|
||
B: TBandSettings;
|
||
Vis: Double;
|
||
begin
|
||
if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit;
|
||
if not FXvtrSettings.Entries[Idx].Enabled then Exit;
|
||
FCurrentXvtr := Idx;
|
||
E := FXvtrSettings.Entries[Idx];
|
||
// Начальная частота: LastFreq если в диапазоне, иначе середина.
|
||
Vis := E.LastFreq;
|
||
if (Vis < E.FreqBegin) or (Vis > E.FreqEnd) then
|
||
Vis := (E.FreqBegin + E.FreqEnd) / 2.0;
|
||
|
||
ApplyXvtrToNetwork; // XVTR enable bit + DisablePA + RX ant
|
||
|
||
// Авто-флаги канала (не входят в TBandSettings/ApplyBandDSP).
|
||
FFMCTCSSAutoActive := E.LastCTCSSAutoActive;
|
||
FFMRptAutoActive := E.LastFMRptAutoActive;
|
||
|
||
// DSP-настройки слота — через общий путь Band (mode/filter/AGC/CTUN/FM + рендер).
|
||
B.Mode := E.LastMode;
|
||
B.FilterIdx := E.LastFilterIdx;
|
||
B.FilterBW := FilterBWFor(E.LastMode, E.LastFilterIdx);
|
||
B.AGCMode := E.LastAGCMode;
|
||
B.AGCTop := E.LastAGCTop;
|
||
B.CTun := E.LastCTun;
|
||
B.SpanHz := FSampleRate;
|
||
B.FMSQOn := E.LastFMSQOn;
|
||
B.FMSQLevel := E.LastFMSQLevel;
|
||
B.CTCSSOn := E.LastCTCSSOn;
|
||
B.CTCSSToneIdx := E.LastCTCSSToneIdx;
|
||
B.FMStepOn := E.LastFMStepOn;
|
||
B.FMStepIdx := E.LastFMStepIdx;
|
||
B.FMRptDir := E.LastFMRptDir;
|
||
B.FMRptOffsetHz := E.LastFMRptOffsetHz;
|
||
B.RxGainMode := E.LastRxGainMode; // hw-AGC слота (AD936x)
|
||
B.RxGainDb := E.LastRxGainDb;
|
||
B.AttenDB := E.LastAttenDB; // аттенюатор ADC слота
|
||
B.VfoA := Vis;
|
||
B.VfoB := FVfoB;
|
||
ApplyBandDSP(B);
|
||
|
||
// Zoom/pan слота: восстанавливаем и переармируем анализатор.
|
||
FZoomFactor := EnsureRange(E.LastZoomFactor, 0.0, 1.0);
|
||
FPanSlider := EnsureRange(E.LastPanSlider, 0.0, 1.0);
|
||
if Assigned(FDSPEngine) then FDSPEngine.SetZoomPan(FZoomFactor, FPanSlider);
|
||
Changed(rfZoom);
|
||
|
||
// VFO: B — своя сохранённая частота слота (клэмп в диапазон); A = visible.
|
||
FVfoB := EnsureRange(E.LastFreqB, E.FreqBegin, E.FreqEnd);
|
||
FVfoA := Vis;
|
||
// При CTun восстанавливаем сохранённый DDC-центр, чтобы VFO остался на своём
|
||
// месте в полосе (а не прыгал в центр). Берём только если offset влезает в
|
||
// текущий span — иначе центрируемся на видимой частоте.
|
||
if FCTun and (E.LastCenterHz > 0) and (Abs(Vis - E.LastCenterHz) < FSpanHz / 2) then
|
||
begin
|
||
FCenterFreq := E.LastCenterHz;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Vis - FCenterFreq);
|
||
end
|
||
else
|
||
begin
|
||
FCenterFreq := Vis;
|
||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
||
end;
|
||
FDriveLevel := CalcDriveByte;
|
||
// QO-100 / split-LO: авто display-duplex (мониторинг своего downlink при TX).
|
||
if E.FullDuplexTx and not FDisplayDuplex then SetDuplex(True);
|
||
PushNetworkState;
|
||
RefreshPanDDCs; // живые паны — в диапазон XVTR, DDC — на IF
|
||
ApplyAntPorts; // AD936x: разъёмы, выбранные для этого трансвертера
|
||
|
||
Changed(rfXvtr);
|
||
Changed(rfActiveVfo);
|
||
end;
|
||
|
||
procedure TRadioController.DeactivateXvtr;
|
||
// Сохраняет текущий XVTR-слот и выходит в HF: HF-возврат (RestoreBand + финальная
|
||
// перестройка активного VFO + ResetWfAvgBuf через rfBandRestore) делается здесь.
|
||
// Changed(rfXvtr) шлём ПОСЛЕ RestoreBand — чтобы wideband/web-extras в его рендере
|
||
// увидели уже восстановленное HF-состояние.
|
||
begin
|
||
if FCurrentXvtr < 0 then Exit;
|
||
SaveCurrentBand; // XVTR-ветка: пишет весь слот + SaveXvtr
|
||
if FDevConnected then FSettings.Save;
|
||
FCurrentXvtr := -1;
|
||
ApplyXvtrToNetwork; // снять XVTR enable bit
|
||
RefreshPanDDCs; // живые паны — обратно в пределы железа
|
||
if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
|
||
RestoreBand(FCurrentBand); // HF-возврат (состояние/DSP/retune)
|
||
Changed(rfXvtr); // погасить XVTR-кнопки + wideband/web (rfXvtr-рендер)
|
||
end;
|
||
|
||
procedure TRadioController.SetXvtrBand(Idx: Integer);
|
||
// Единая точка для GUI/Web/CAT: Idx>=0 — войти в слот, Idx<0 — выйти в HF.
|
||
// ВНИМАНИЕ: HF-возврат при выходе делает UI-обёртка DeactivateXvtr (RestoreBand).
|
||
begin
|
||
if Idx >= 0 then ActivateXvtr(Idx)
|
||
else DeactivateXvtr;
|
||
end;
|
||
|
||
procedure TRadioController.SetCurrentBandIdx(NewBand: Integer);
|
||
// Лёгкая смена HF-диапазона без полного restore: фиксируем индекс диапазона
|
||
// (для drive-кал и подсветки кнопок) и пересчитываем drive-байт. Используется
|
||
// канальным адаптером при переходе канала на другой HF-диапазон.
|
||
begin
|
||
FCurrentBand := NewBand;
|
||
FDriveLevel := CalcDriveByte;
|
||
Changed(rfBand);
|
||
end;
|
||
|
||
procedure TRadioController.SetSpan(Hz: Integer);
|
||
begin FSpanHz := Hz; Changed(rfSpan); { TODO wiring } end;
|
||
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
// Zoom / pan спектра и водопада
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
|
||
function TRadioController.ZoomFactor: Double;
|
||
begin Result := FZoomFactor; end;
|
||
|
||
function TRadioController.PanSlider: Double;
|
||
begin Result := FPanSlider; end;
|
||
|
||
procedure ViewWindowFor(BaseC, BaseS, AZoom, APan: Double;
|
||
out ACenter, ASpan: Double);
|
||
// Видимое окно (центр+span, Гц) с учётом зума. Формула совпадает с расчётом
|
||
// span-clip в WDSPEngine.Apply{RX,Pan}AnalyzerSettings (bw=BaseS, clp=0),
|
||
// поэтому оверлеи точно ложатся на пиксели. Без зума → базовые центр/span.
|
||
var
|
||
FVis, OneMinusFVis, VisSpan, LowEdgeOff, CenterOff: Double;
|
||
begin
|
||
if (AZoom <= 0.0) or (BaseS <= 0.0) then
|
||
begin
|
||
ACenter := BaseC;
|
||
ASpan := BaseS;
|
||
Exit;
|
||
end;
|
||
OneMinusFVis := 0.99 * Log10(9.0 * EnsureRange(AZoom, 0.0, 1.0) + 1.0);
|
||
FVis := 1.0 - OneMinusFVis; // доля span после зума
|
||
if FVis < 1.0 / 100.0 then FVis := 1.0 / 100.0; // ZOOM_LIMIT
|
||
VisSpan := FVis * BaseS;
|
||
LowEdgeOff := -BaseS / 2.0 + EnsureRange(APan, 0.0, 1.0) * OneMinusFVis * BaseS;
|
||
CenterOff := LowEdgeOff + VisSpan / 2.0;
|
||
ACenter := BaseC + CenterOff;
|
||
ASpan := VisSpan;
|
||
end;
|
||
|
||
procedure TRadioController.GetViewWindow(out ACenter: Double; out ASpan: Double);
|
||
begin
|
||
ViewWindowFor(FCenterFreq, FSpanHz, FZoomFactor, FPanSlider, ACenter, ASpan);
|
||
end;
|
||
|
||
function TRadioController.AnchoredPan(AZoom: Double): Double;
|
||
// Положение пана, при котором видимое окно центрировано на маркере активного
|
||
// VFO. Клампится в [0..1], чтобы окно не вылезало за span (у краёв маркер
|
||
// перестаёт быть строго по центру — окно упирается в край).
|
||
var OneMinusFVis, U: Double;
|
||
begin
|
||
if (AZoom <= 0.0) or (FSpanHz <= 0.0) then Exit(0.5);
|
||
OneMinusFVis := 0.99 * Log10(9.0 * EnsureRange(AZoom, 0.0, 1.0) + 1.0);
|
||
if OneMinusFVis <= 1e-9 then Exit(0.5);
|
||
U := (ActiveVfoHz - FCenterFreq) / FSpanHz; // нормализованный сдвиг маркера
|
||
Result := EnsureRange(0.5 + U / OneMinusFVis, 0.0, 1.0);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyZoom(AZoom, APan: Double);
|
||
begin
|
||
FZoomFactor := EnsureRange(AZoom, 0.0, 1.0);
|
||
FPanSlider := EnsureRange(APan, 0.0, 1.0);
|
||
if Assigned(FDSPEngine) then
|
||
FDSPEngine.SetZoomPan(FZoomFactor, FPanSlider);
|
||
// Кэшируем в активный слот (XVTR-слот при активном трансверторе, иначе HF-band);
|
||
// персист на диск — в SaveCurrentBand при смене/выходе.
|
||
if FCurrentXvtr >= 0 then
|
||
begin
|
||
if FCurrentXvtr < CFG_XVTR_COUNT then
|
||
begin
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastZoomFactor := FZoomFactor;
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastPanSlider := FPanSlider;
|
||
end;
|
||
end
|
||
else if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
|
||
begin
|
||
FBandCache[FCurrentBand].ZoomFactor := FZoomFactor;
|
||
FBandCache[FCurrentBand].PanSlider := FPanSlider;
|
||
end;
|
||
Changed(rfZoom);
|
||
end;
|
||
|
||
// Лестница кратностей увеличения: мелкий шаг в начале (1.0→1.2→1.45…), крупнее
|
||
// к концу. Кнопки [+]/[-] шагают по ней; ZoomFactor (0..1) хранит соответствующий
|
||
// z. Перевод кратность↔z — инверсия формулы анализатора (см. ApplyRXAnalyzerSettings).
|
||
const
|
||
ZOOM_MAG_LADDER: array[0..16] of Double =
|
||
(1.0, 1.1, 1.15, 1.2, 1.45, 1.75, 2.1, 2.5, 3.0, 3.7, 4.7, 6.2, 8.5, 12.0, 20.0, 40.0, 100.0);
|
||
|
||
function ZoomMagToZ(M: Double): Double;
|
||
begin
|
||
if M <= 1.0 then Exit(0.0);
|
||
// M = 1/(1-0.99*log10(9z+1)) ⇒ z = (10^((1-1/M)/0.99) - 1)/9
|
||
Result := EnsureRange((Power(10.0, (1.0 - 1.0 / M) / 0.99) - 1.0) / 9.0, 0.0, 1.0);
|
||
end;
|
||
|
||
function ZoomZToMag(Z: Double): Double;
|
||
begin
|
||
Result := 1.0 / (1.0 - 0.99 * Log10(9.0 * EnsureRange(Z, 0.0, 1.0) + 1.0));
|
||
end;
|
||
|
||
procedure TRadioController.ZoomIn;
|
||
var CurMag, Z: Double; i: Integer;
|
||
begin
|
||
CurMag := ZoomZToMag(FZoomFactor);
|
||
for i := 0 to High(ZOOM_MAG_LADDER) do
|
||
if ZOOM_MAG_LADDER[i] > CurMag * 1.001 then
|
||
begin
|
||
Z := ZoomMagToZ(ZOOM_MAG_LADDER[i]);
|
||
ApplyZoom(Z, AnchoredPan(Z));
|
||
Exit;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.ZoomOut;
|
||
var CurMag, Z: Double; i: Integer;
|
||
begin
|
||
CurMag := ZoomZToMag(FZoomFactor);
|
||
for i := High(ZOOM_MAG_LADDER) downto 0 do
|
||
if ZOOM_MAG_LADDER[i] < CurMag / 1.001 then
|
||
begin
|
||
if ZOOM_MAG_LADDER[i] <= 1.0 then ApplyZoom(0.0, 0.5)
|
||
else
|
||
begin
|
||
Z := ZoomMagToZ(ZOOM_MAG_LADDER[i]);
|
||
ApplyZoom(Z, AnchoredPan(Z));
|
||
end;
|
||
Exit;
|
||
end;
|
||
ApplyZoom(0.0, 0.5);
|
||
end;
|
||
|
||
procedure TRadioController.ZoomDefault;
|
||
begin
|
||
ApplyZoom(0.0, 0.5);
|
||
end;
|
||
|
||
{ ---- Зум/пан доп. панов (PanId=0 делегирует главному) ---- }
|
||
|
||
procedure TRadioController.GetPanViewWindow(PanId: Integer;
|
||
out ACenter, ASpan: Double);
|
||
begin
|
||
if PanId = 0 then
|
||
begin
|
||
GetViewWindow(ACenter, ASpan);
|
||
Exit;
|
||
end;
|
||
ACenter := 0; ASpan := 0;
|
||
if not PanDDCActive(PanId) then Exit;
|
||
ViewWindowFor(FPanFreqHz[PanId], FPanRateKHz[PanId] * 1000.0,
|
||
FPanZoomF[PanId], FPanPanSl[PanId], ACenter, ASpan);
|
||
end;
|
||
|
||
procedure TRadioController.SetPanZoomPan(PanId: Integer; AZoom, APan: Double);
|
||
// Зум пана — состояние сессии пана (не персистится с бандом, в отличие от
|
||
// главного ApplyZoom): пан живёт до STOP.
|
||
begin
|
||
if PanId = 0 then
|
||
begin
|
||
ApplyZoom(AZoom, APan);
|
||
Exit;
|
||
end;
|
||
if not PanDDCActive(PanId) then Exit;
|
||
FPanZoomF[PanId] := EnsureRange(AZoom, 0.0, 1.0);
|
||
FPanPanSl[PanId] := EnsureRange(APan, 0.0, 1.0);
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetPanZoomPan(PanId, FPanZoomF[PanId], FPanPanSl[PanId]);
|
||
end;
|
||
|
||
procedure TRadioController.PanZoomStepTo(PanId: Integer; NewZ: Double);
|
||
// Ставит новый зум, удерживая ЦЕНТР текущего видимого окна (у пана нет VFO-якоря
|
||
// главного AnchoredPan; фиксация центра — естественный аналог).
|
||
var
|
||
C, S, BaseC, BaseS, U, OneMinus, FVis, NewPan: Double;
|
||
begin
|
||
if not PanDDCActive(PanId) then Exit;
|
||
GetPanViewWindow(PanId, C, S);
|
||
BaseC := FPanFreqHz[PanId];
|
||
BaseS := FPanRateKHz[PanId] * 1000.0;
|
||
if BaseS <= 0 then Exit;
|
||
NewZ := EnsureRange(NewZ, 0.0, 1.0);
|
||
// Инверсия ViewWindowFor: U = CenterOff/BaseS = −0.5 + Pan·OneMinus + FVis/2.
|
||
U := (C - BaseC) / BaseS;
|
||
OneMinus := 0.99 * Log10(9.0 * NewZ + 1.0);
|
||
if OneMinus <= 1e-9 then
|
||
NewPan := 0.5
|
||
else
|
||
begin
|
||
FVis := 1.0 - OneMinus;
|
||
if FVis < 1.0 / 100.0 then FVis := 1.0 / 100.0;
|
||
NewPan := EnsureRange((U + 0.5 - FVis / 2.0) / OneMinus, 0.0, 1.0);
|
||
end;
|
||
SetPanZoomPan(PanId, NewZ, NewPan);
|
||
end;
|
||
|
||
procedure TRadioController.PanZoomIn(PanId: Integer);
|
||
var CurMag: Double; i: Integer;
|
||
begin
|
||
if not PanDDCActive(PanId) then Exit;
|
||
CurMag := ZoomZToMag(FPanZoomF[PanId]);
|
||
for i := 0 to High(ZOOM_MAG_LADDER) do
|
||
if ZOOM_MAG_LADDER[i] > CurMag * 1.001 then
|
||
begin
|
||
PanZoomStepTo(PanId, ZoomMagToZ(ZOOM_MAG_LADDER[i]));
|
||
Exit;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.PanZoomOut(PanId: Integer);
|
||
var CurMag: Double; i: Integer;
|
||
begin
|
||
if not PanDDCActive(PanId) then Exit;
|
||
CurMag := ZoomZToMag(FPanZoomF[PanId]);
|
||
for i := High(ZOOM_MAG_LADDER) downto 0 do
|
||
if ZOOM_MAG_LADDER[i] < CurMag / 1.001 then
|
||
begin
|
||
if ZOOM_MAG_LADDER[i] <= 1.0 then SetPanZoomPan(PanId, 0.0, 0.5)
|
||
else PanZoomStepTo(PanId, ZoomMagToZ(ZOOM_MAG_LADDER[i]));
|
||
Exit;
|
||
end;
|
||
SetPanZoomPan(PanId, 0.0, 0.5);
|
||
end;
|
||
|
||
procedure TRadioController.PanZoomDefault(PanId: Integer);
|
||
begin
|
||
SetPanZoomPan(PanId, 0.0, 0.5);
|
||
end;
|
||
|
||
function TRadioController.PanZoomFactor(PanId: Integer): Double;
|
||
begin
|
||
if PanId = 0 then Result := FZoomFactor
|
||
else if PanDDCActive(PanId) then Result := FPanZoomF[PanId]
|
||
else Result := 0.0;
|
||
end;
|
||
|
||
function TRadioController.PanPanSlider(PanId: Integer): Double;
|
||
begin
|
||
if PanId = 0 then Result := FPanSlider
|
||
else if PanDDCActive(PanId) then Result := FPanPanSl[PanId]
|
||
else Result := 0.5;
|
||
end;
|
||
|
||
procedure TRadioController.SetSampleRate(Hz: Integer);
|
||
// Смена частоты дискретизации DDC. В этом приложении span ≡ sample rate.
|
||
// Несёт: пересоздание WDSP-канала (Close+FlushQueue+Open), новый rate в
|
||
// трансивер и полное восстановление DSP-состояния. Команда сама персистит
|
||
// (sample rate глобальный); рендер overlay/AGC-линий — в OnControllerState.
|
||
begin
|
||
if (Hz <= 0) or (Hz = FSampleRate) then Exit;
|
||
FSampleRate := Hz;
|
||
FSpanHz := Hz;
|
||
FBandCache[FCurrentBand].SpanHz := FSpanHz; // для обратной совместимости
|
||
|
||
// 1. Меняем rate главного тракта. Хирургически: движок гейтит и дропает
|
||
// только main-пакеты, DSP-поток/анализаторы/паны N живут без пауз —
|
||
// поэтому FWDSPReady НЕ сбрасываем (иначе OnDDCIQ перестал бы кормить
|
||
// паны на всё время пересборки и их водопады замирали).
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.ChangeSampleRate(Hz);
|
||
|
||
// 2. Новый rate трансиверу. Короткий хвост пакетов старого rate, дошедших
|
||
// до переключения радио, попадает в новый канал — транзиент в единицы мс
|
||
// (то же окно было и при старой схеме: FWDSPReady поднимался до того, как
|
||
// радио реально применяло DDC Specific).
|
||
if Assigned(FNetwork) and FNetwork.Connected then
|
||
FNetwork.ConfigureDDCs(1, Hz div 1000, FMainADCSrc,
|
||
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;
|
||
SaveGlobalSettings; // sample rate глобальный — персистим (гейт по FDevConnected внутри)
|
||
Changed(rfSampleRate);
|
||
end;
|
||
|
||
procedure TRadioController.SetCTun(On_: Boolean);
|
||
begin
|
||
FCTun := On_;
|
||
if XvtrSlotActive then FXvtrSettings.Entries[FCurrentXvtr].LastCTun := FCTun
|
||
else 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_;
|
||
// Применяем сразу: при передаче держим RX-IQ в DSP-цепи (full-duplex монитор —
|
||
// для QO-100 слышим свой downlink через спутник). SetMOX тоже это выставляет,
|
||
// но здесь — чтобы тумблер DUP действовал и в момент передачи.
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.KeepRXDuringTX := FDisplayDuplex;
|
||
ApplyActiveVolume; // смена DUP на передаче может включить/выключить TX-монитор
|
||
Changed(rfDuplex);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyRxMuteToEngine;
|
||
begin
|
||
// Монитор имеет смысл только в full-duplex (KeepRXDuringTX); сам гейт в движке
|
||
// это учитывает. Здесь просто транслируем инверсию флага RX MUTE.
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.MonitorRXDuringTX := not FRxMuteOnTx;
|
||
end;
|
||
|
||
procedure TRadioController.SetRxMuteOnTx(On_: Boolean);
|
||
begin
|
||
FRxMuteOnTx := On_;
|
||
ApplyRxMuteToEngine;
|
||
ApplyActiveVolume; // на передаче тумблер монитора меняет активную громкость
|
||
Changed(rfRxMuteOnTx);
|
||
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;
|
||
|
||
function TRadioController.DefaultMicSource: TTXMicSource;
|
||
// Источник модуляции решает ТЕКУЩИЙ TX-слайс: у него может быть свой вход
|
||
// (виртуальный кабель), даже когда общего входа нет, и наоборот.
|
||
begin
|
||
if ActiveMicInDevName <> '' then Result := txmsSoundCard
|
||
else Result := txmsRadio;
|
||
end;
|
||
|
||
function TRadioController.BuildMicLineSelectByte: Byte;
|
||
// openHPSDR Ethernet Protocol v4.3, DUC Specific byte 50:
|
||
// bit 0 — Line In (1 = line-in вместо mic-in)
|
||
// bit 1 — Mic Boost (+20 dB); не ставим когда Line In
|
||
// bit 2 — 0 = Orion mic PTT enabled, 1 = disabled (инверсия!)
|
||
// bit 3 — Tip/Ring (0=Tip, 1=Ring; Orion/OrionMkII)
|
||
// bit 4 — Orion mic bias (1 = enable +5V bias)
|
||
// bit 5 — Saturn XLR/balanced (не используем)
|
||
begin
|
||
Result := 0;
|
||
if FTXSettings.MicLineIn then
|
||
Result := Result or $01 // bit 0: Line In
|
||
else if FTXSettings.MicBoost then
|
||
Result := Result or $02; // bit 1: Boost только при Mic In
|
||
if not FTXSettings.MicPTTEnabled then Result := Result or $04; // bit 2: инверсия
|
||
if FTXSettings.MicTipRing then Result := Result or $08; // bit 3: Ring
|
||
if FTXSettings.MicBias then Result := Result or $10; // bit 4: bias
|
||
end;
|
||
|
||
procedure TRadioController.ApplyNoiseFilterButtonsToDSP;
|
||
begin
|
||
if not FWDSPReady then Exit;
|
||
FDSPEngine.SetNRMode(FNRMode);
|
||
FDSPEngine.SetNBMode(FNBMode);
|
||
FDSPEngine.SetSNB(FSNB);
|
||
FDSPEngine.SetANF(FANF);
|
||
end;
|
||
|
||
function TRadioController.XvtrSlotActive: Boolean;
|
||
begin
|
||
Result := (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT);
|
||
end;
|
||
|
||
function TRadioController.TXProhibited: Boolean;
|
||
// Единая точка запретов передачи: диапазон с DoNotTx (Alex) и RX-only слот
|
||
// трансвертера. Раньше эта пара была скопирована в SetMOX/SetTune/SetTwoTone —
|
||
// то есть жила ТОЛЬКО на пути через софтовый MOX. С кейером в прошивке этого
|
||
// пути нет вовсе, и на RX-only трансвертере ключ спокойно выходил в эфир.
|
||
begin
|
||
Result := (FDevConnected and (FCurrentXvtr < 0)
|
||
and FAlexSettings.DoNotTx[EnsureRange(TxBandIdx, 0, 10)])
|
||
or ((FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
|
||
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly);
|
||
end;
|
||
|
||
function TRadioController.CWFWSource: Boolean;
|
||
// Манипуляцию рисует ПРОШИВКА (кейер в FPGA, openHPSDR P2). Путь по умолчанию:
|
||
// тайминги в железе, джиттер PC не влияет вовсе.
|
||
begin
|
||
Result := BackendCaps.HasCWKeyer and FCWSettings.FWKeyer
|
||
and (not FCWSettings.LocalKeyer);
|
||
end;
|
||
|
||
function TRadioController.CWLocalSource: Boolean;
|
||
// Манипуляцию рисует САМ EWSDR (CWKeyer.pas). Выбирается списком в настройках, а
|
||
// на железе без кейера в прошивке (Pluto/LibreSDR — голый AD936x) выбор
|
||
// «прошивка» вырождается сюда: другого пути там просто нет.
|
||
// ★Оба источника False = кейер выключен намеренно (третий пункт списка): тогда
|
||
// телеграф идёт только через MOX, и ключевать некому.
|
||
begin
|
||
Result := FCWSettings.LocalKeyer
|
||
or ((not BackendCaps.HasCWKeyer) and FCWSettings.FWKeyer);
|
||
end;
|
||
|
||
function TRadioController.CWTXActive: Boolean;
|
||
// «Передачей в телеграфе владеет не MOX»: режим ИСТОЧНИКА передачи — CW и
|
||
// манипуляция кому-то поручена (прошивке или локальному генератору). В этом
|
||
// состоянии голосовой TXA не запускается вовсе (эталон Thetis, console.cs:29570),
|
||
// а несущую даёт кейер / бит CWX / тон TUN.
|
||
begin
|
||
Result := (ActiveTXMode in [MODE_CWL, MODE_CWU])
|
||
and (CWFWSource or CWLocalSource)
|
||
and (not TXProhibited); // где передавать нельзя — кейер разоружён
|
||
end;
|
||
|
||
function TRadioController.CWCarrierOffsetHz: Double;
|
||
// Сдвиг несущей манипуляции от гетеродина — и в генераторе, и в частоте DUC/LO.
|
||
// ★У zero-IF (AD936x) ноль бейсбенда = частота гетеродина, и ТАМ ЖЕ сидит его
|
||
// утечка. Держи несущую на нуле — между посылками в эфир пойдёт слабый backwave
|
||
// РОВНО на рабочей частоте (самое неприятное, что бывает в телеграфе). Поэтому
|
||
// у Pluto манипуляция идёт на сдвиге, а LO уезжает навстречу (знак — в
|
||
// TXTuneFreqHz, он замерен на эфире). У openHPSDR DUC цифровой, утекать
|
||
// нечему — сдвиг 0.
|
||
const
|
||
CW_AUTO_OFFSET_HZ = 12000;
|
||
begin
|
||
Result := 0;
|
||
if not FCWLocalArmed then Exit;
|
||
// TUN/2TON идут через TXA (несущую даёт SSB-модулятор из тона), их частота
|
||
// уже посчитана в ActiveTXFreqHz — сдвигать нечего.
|
||
if FTuning or FTwoTone then Exit;
|
||
// −1 = «без сдвига» ЯВНО (несущая прямо на гетеродине). Нужно не только для
|
||
// отладки: сдвиг разносит TX LO и RX LO на единицы-десятки килогерц, а два
|
||
// fractional-N синтезатора в одном кристалле на близких частотах тянут друг
|
||
// друга и дают симметричные боковые вокруг несущей. Что хуже — утечка LO на
|
||
// рабочей частоте или эти боковые, решает оператор на своём железе.
|
||
if FCWSettings.CarrierOffsetHz < 0 then Exit;
|
||
if FCWSettings.CarrierOffsetHz > 0 then
|
||
Result := FCWSettings.CarrierOffsetHz
|
||
else if not BackendCaps.HasCWKeyer then
|
||
Result := CW_AUTO_OFFSET_HZ;
|
||
end;
|
||
|
||
function TRadioController.CWOptionsByte: Byte;
|
||
// DUC Specific байт 5. Порт pihpsdr new_protocol.c:1353-1383 (там же
|
||
// объяснение, почему iambic-бит выставляется и для Mode B).
|
||
// ★Вне телеграфа байт ОБЯЗАН быть нулевым: иначе прошивка сама поднимет PTT
|
||
// на замыкание ключа посреди SSB (Thetis, console.cs:27394).
|
||
begin
|
||
Result := 0;
|
||
// ★Байт обязан быть нулевым не только вне телеграфа, но и когда манипуляцию
|
||
// взял на себя локальный генератор: иначе прошивка ключевала бы ПАРАЛЛЕЛЬНО
|
||
// с ним (и поднимала бы PTT на замыкание своего разъёма).
|
||
if not (CWTXActive and CWFWSource) then Exit;
|
||
Result := Result or CW_MODE;
|
||
if FCWSettings.ReverseKeys then Result := Result or CW_REVERSE_KEYS;
|
||
case FCWSettings.KeyerMode of
|
||
1: Result := Result or CW_IAMBIC; // Mode A
|
||
2: Result := Result or CW_IAMBIC or CW_MODE_B; // Mode B
|
||
end; // 0 = прямой ключ
|
||
if FCWSettings.SidetoneHW and (FCWSettings.SidetoneHWLevel > 0) then
|
||
Result := Result or CW_SIDETONE;
|
||
if FCWSettings.StrictSpacing then Result := Result or CW_STRICT_SPACING;
|
||
if FCWSettings.BreakIn then Result := Result or CW_BREAK_IN;
|
||
end;
|
||
|
||
procedure TRadioController.SendDUCSpecificFromSettings;
|
||
var
|
||
DUCPkt: TDUCSpecificPacket;
|
||
RateKsps: Word;
|
||
HangMs: Word;
|
||
begin
|
||
// Assigned обязателен: сюда приходят и по PTT/TUN, а нажать их можно и до
|
||
// того, как устройство выбрано, — тогда FNetwork ещё nil (то же и с движком
|
||
// ниже). Раньше на этом месте была Access violation, и разбирать её приходил
|
||
// уже кто-то другой: SetMOX зовёт SyncCWKeyer, а тот — эту процедуру.
|
||
if not (Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running) then Exit;
|
||
if not (FWDSPReady and Assigned(FDSPEngine)) then Exit;
|
||
FillChar(DUCPkt, SizeOf(DUCPkt), 0);
|
||
// DUC0 rate в ksps берём из WDSP-движка (фиксированный TX rate, не RX).
|
||
RateKsps := FDSPEngine.TXSampleRate div 1000;
|
||
DUCPkt.NumDACs := 1;
|
||
DUCPkt.CWOptions := CWOptionsByte;
|
||
// Уровень/частота аппаратного сайдтона — тон в наушниках САМОГО трансивера.
|
||
if FCWSettings.SidetoneHW then
|
||
DUCPkt.SidetoneLevel := Byte(EnsureRange(FCWSettings.SidetoneHWLevel, 0, 127))
|
||
else
|
||
DUCPkt.SidetoneLevel := 0;
|
||
DUCPkt.SidetoneFreqHi := Hi(Word(EnsureRange(FCWSettings.Pitch, 200, 1200)));
|
||
DUCPkt.SidetoneFreqLo := Lo(Word(EnsureRange(FCWSettings.Pitch, 200, 1200)));
|
||
DUCPkt.KeyerSpeed := Byte(EnsureRange(FCWSettings.Speed, 1, 60));
|
||
DUCPkt.KeyerWeight := Byte(EnsureRange(FCWSettings.Weight, 33, 66));
|
||
// Hang — сколько прошивка держит передачу после последнего элемента (semi
|
||
// break-in). При выключенном break-in железу его знать незачем.
|
||
if FCWSettings.BreakIn then
|
||
HangMs := Word(EnsureRange(FCWSettings.HangTimeMS, 0, 2000))
|
||
else
|
||
HangMs := 0;
|
||
DUCPkt.HangDelayHi := Hi(HangMs);
|
||
DUCPkt.HangDelayLo := Lo(HangMs);
|
||
DUCPkt.RFDelay := Byte(EnsureRange(FCWSettings.RFDelayMS, 0, 255));
|
||
DUCPkt.CWRampPeriod := Byte(EnsureRange(FCWSettings.RampMS, 0, 20));
|
||
DUCPkt.DUC0RateHi := Hi(RateKsps);
|
||
DUCPkt.DUC0RateLo := Lo(RateKsps);
|
||
DUCPkt.DUC0Bits := 24;
|
||
DUCPkt.MicLineSelect := BuildMicLineSelectByte;
|
||
// Line In Gain (byte 51): 0=-34.5dB .. 31=+12dB (шаг 1.5 dB, формула Thetis).
|
||
// При Mic In значение игнорируется железом, но шлём корректное для Line In.
|
||
DUCPkt.LineInGain := Byte(EnsureRange(Round((FTXSettings.LineInGainDB + 34.5) / 1.5), 0, 31));
|
||
// ATT on TX — RX-ADC step attenuator во время передачи (Thetis-style protection).
|
||
// Дублируем значение во все три ADC-байта: на однопотоковых платах активен только
|
||
// ADC0, на Saturn/Hermes Lite 2 — могут использоваться ADC1/ADC2.
|
||
DUCPkt.StepAtten0 := Byte(EnsureRange(FTXSettings.AttOnTX, 0, 31));
|
||
DUCPkt.StepAtten1 := DUCPkt.StepAtten0;
|
||
DUCPkt.StepAtten2 := DUCPkt.StepAtten0;
|
||
FLastCWOptions := DUCPkt.CWOptions;
|
||
FNetwork.SendDUCSpecific(DUCPkt);
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// TX-профили
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function TRadioController.TXProfileCount: Integer;
|
||
begin
|
||
Result := FTXProfiles.Count;
|
||
end;
|
||
|
||
function TRadioController.TXProfileName(Idx: Integer): string;
|
||
begin
|
||
if (Idx < 0) or (Idx >= FTXProfiles.Count) then Result := ''
|
||
else Result := FTXProfiles.Items[Idx].Name;
|
||
end;
|
||
|
||
function TRadioController.ActiveTXProfile: Integer;
|
||
begin
|
||
Result := FTXProfiles.ActiveIdx;
|
||
end;
|
||
|
||
procedure TRadioController.SaveTXProfilesToConfig;
|
||
begin
|
||
if (not FDevConnected) or (not Assigned(FSettings)) then Exit;
|
||
FSettings.SaveTXProfiles(FDevMAC, FTXProfiles);
|
||
FSettings.Save;
|
||
end;
|
||
|
||
procedure TRadioController.SetTXSettings(const T: TTXSettings; Notify: Boolean);
|
||
// Одна дверь для правки TX-настроек — по образцу SetCWSettings. Клиенту
|
||
// (вкладка Transmit, CAT, web) достаточно отдать запись целиком: применение к
|
||
// WDSP, перепосыл DUC Specific при смене mic-битов/ATT, живое обновление TUN,
|
||
// запись в активный профиль и персист — здесь.
|
||
var
|
||
OldMicByte: Byte;
|
||
OldAtt, OldTUNLevel, OldTUNFreq: Integer;
|
||
begin
|
||
OldMicByte := BuildMicLineSelectByte;
|
||
OldAtt := FTXSettings.AttOnTX;
|
||
OldTUNLevel := FTXSettings.TUNLevel;
|
||
OldTUNFreq := FTXSettings.TUNFreq;
|
||
FTXSettings := T;
|
||
// TX-сетка спектра — рабочие поля рендера; их перерисовку заказывает вызывающий.
|
||
FTXSpecRefLevel := FTXSettings.TXSpecRefLevel;
|
||
FTXSpecRange := FTXSettings.TXSpecRange;
|
||
if FTXSettings.TXSpecGridStep > 0 then FTXSpecGridStep := FTXSettings.TXSpecGridStep;
|
||
ApplyTXSettingsToDSP;
|
||
// Маршрутизация микрофона и ATT on TX живут в железе (DUC Specific), WDSP тут
|
||
// ни при чём — пакет перепосылаем только когда байты реально поменялись.
|
||
if (BuildMicLineSelectByte <> OldMicByte) or (OldAtt <> FTXSettings.AttOnTX) then
|
||
SendDUCSpecificFromSettings;
|
||
// TUN-параметры live: идёт настройка — обновить тон/уровень, иначе просто запомнить.
|
||
if FTuning then
|
||
begin
|
||
if (OldTUNFreq <> FTXSettings.TUNFreq) and FWDSPReady then
|
||
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
|
||
if OldTUNLevel <> FTXSettings.TUNLevel then
|
||
begin
|
||
FDriveLevel := CalcDriveByte;
|
||
if FWDSPReady then
|
||
FDSPEngine.SetDriveLevel(EnsureRange(FTXSettings.TUNLevel, 0, 100) / 100.0);
|
||
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz, FDriveLevel,
|
||
FTransmitting, True, True, FTuning, FTwoTone);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
end;
|
||
// Правка «звуковых» полей — это правка активного профиля: отдельной кнопки
|
||
// «сохранить» нет, профиль ведётся за живым состоянием (см. TTXProfile).
|
||
StoreActiveTXProfile;
|
||
if FDevConnected and Assigned(FSettings) then
|
||
begin
|
||
FSettings.SaveTX(FDevMAC, FTXSettings);
|
||
FSettings.SaveTXProfiles(FDevMAC, FTXProfiles);
|
||
FSettings.Save;
|
||
end;
|
||
if Notify then Changed(rfTXProfile);
|
||
end;
|
||
|
||
procedure TRadioController.StoreActiveTXProfile(Persist: Boolean = False);
|
||
// Живое состояние → активный профиль. Зовётся на каждой правке TX-настроек и
|
||
// на смене drive; Persist=False не трогает диск (drive ездит слайдером).
|
||
var Idx: Integer;
|
||
begin
|
||
Idx := FTXProfiles.ActiveIdx;
|
||
if (Idx < 0) or (Idx >= FTXProfiles.Count) then Exit;
|
||
TXProfileCapture(FTXSettings, FDrivePercent, FTXProfiles.Items[Idx]);
|
||
if Persist then SaveTXProfilesToConfig;
|
||
end;
|
||
|
||
procedure TRadioController.SelectTXProfile(Idx: Integer; Auto: Boolean);
|
||
var
|
||
OldMic: Byte;
|
||
NewDrive: Integer;
|
||
begin
|
||
if (Idx < 0) or (Idx >= FTXProfiles.Count) then Exit;
|
||
// Уходящий профиль дописываем живыми значениями — ручные правки не теряются.
|
||
StoreActiveTXProfile;
|
||
OldMic := BuildMicLineSelectByte;
|
||
NewDrive := FDrivePercent;
|
||
FTXProfiles.ActiveIdx := Idx;
|
||
TXProfileApply(FTXProfiles.Items[Idx], FTXSettings, NewDrive);
|
||
ApplyTXSettingsToDSP;
|
||
// Маршрутизация микрофона живёт в железе (DUC Specific byte 50) — при смене
|
||
// Mic In/Line In/boost/bias пакет надо перепослать, WDSP тут ни при чём.
|
||
if BuildMicLineSelectByte <> OldMic then SendDUCSpecificFromSettings;
|
||
// SetDrive пересчитает калиброванный байт, толкнёт WDSP/Pluto и HP-кадр;
|
||
// зовём безусловно — при активном TUN уровень берётся из TUNLevel профиля.
|
||
SetDrive(NewDrive);
|
||
if FTuning and FWDSPReady then
|
||
FDSPEngine.SetDriveLevel(EnsureRange(FTXSettings.TUNLevel, 0, 100) / 100.0);
|
||
// ★Ручной выбор ЗАПОМИНАЕТСЯ за парой «источник передачи + его модуляция» —
|
||
// это и есть обещанное «профиль помнится по виду модуляции». Без этого в
|
||
// таблице оказывалось только то, что назначено пилюлей во флаге, а профиль,
|
||
// выбранный дропдауном в блоке TX, нигде не оставался следа.
|
||
if not Auto then BindTXProfileToCurrentMode(Idx);
|
||
if FDevConnected and Assigned(FSettings) then
|
||
begin
|
||
FSettings.SaveTX(FDevMAC, FTXSettings);
|
||
FSettings.SaveTXProfiles(FDevMAC, FTXProfiles);
|
||
FSettings.Save;
|
||
end;
|
||
Changed(rfTXProfile);
|
||
end;
|
||
|
||
function TRadioController.AddTXProfile(const AName: string): Integer;
|
||
// Форк: новый профиль = текущий звук под новым именем, он же становится активным.
|
||
begin
|
||
Result := -1;
|
||
if FTXProfiles.Count >= TXPROF_MAX then Exit;
|
||
Result := FTXProfiles.Count;
|
||
TXProfileCapture(FTXSettings, FDrivePercent, FTXProfiles.Items[Result]);
|
||
FTXProfiles.Items[Result].Name := Copy(Trim(AName), 1, 31);
|
||
if FTXProfiles.Items[Result].Name = '' then
|
||
FTXProfiles.Items[Result].Name := 'Profile ' + IntToStr(Result + 1);
|
||
Inc(FTXProfiles.Count);
|
||
FTXProfiles.ActiveIdx := Result;
|
||
SaveTXProfilesToConfig;
|
||
Changed(rfTXProfile);
|
||
end;
|
||
|
||
procedure TRadioController.RenameTXProfile(Idx: Integer; const AName: string);
|
||
var S: string;
|
||
begin
|
||
if (Idx < 0) or (Idx >= FTXProfiles.Count) then Exit;
|
||
S := Copy(Trim(AName), 1, 31);
|
||
if S = '' then Exit;
|
||
FTXProfiles.Items[Idx].Name := S;
|
||
SaveTXProfilesToConfig;
|
||
Changed(rfTXProfile);
|
||
end;
|
||
|
||
procedure TRadioController.DeleteTXProfile(Idx: Integer);
|
||
// Последний профиль не удаляем: список без активного элемента сломал бы
|
||
// авто-сохранение (StoreActiveTXProfile писать некуда).
|
||
var i: Integer;
|
||
begin
|
||
if (Idx < 0) or (Idx >= FTXProfiles.Count) or (FTXProfiles.Count <= 1) then Exit;
|
||
for i := Idx to FTXProfiles.Count - 2 do
|
||
FTXProfiles.Items[i] := FTXProfiles.Items[i + 1];
|
||
Dec(FTXProfiles.Count);
|
||
// Привязки флагов — это ИНДЕКСЫ: после сдвига списка их надо переписать,
|
||
// иначе слайс молча начнёт включать соседний профиль.
|
||
RemapTXProfileBindings(Idx);
|
||
if FTXProfiles.ActiveIdx > Idx then Dec(FTXProfiles.ActiveIdx)
|
||
else if FTXProfiles.ActiveIdx = Idx then
|
||
begin
|
||
// Удалили активный: встаём на соседний и применяем его в эфир. ActiveIdx
|
||
// гасим — иначе StoreActiveTXProfile внутри Select затрёт соседа живым
|
||
// звуком удалённого профиля.
|
||
FTXProfiles.ActiveIdx := -1;
|
||
SelectTXProfile(EnsureRange(Idx, 0, FTXProfiles.Count - 1));
|
||
Exit; // SelectTXProfile уже сохранил и отрендерил
|
||
end;
|
||
SaveTXProfilesToConfig;
|
||
Changed(rfTXProfile);
|
||
end;
|
||
|
||
procedure TRadioController.RemapTXProfileBindings(Deleted: Integer);
|
||
// Профиль удалён из середины списка: привязку на него снимаем, привязки на
|
||
// профили правее — сдвигаем. Deleted < 0 = список заменён целиком, снимаем все.
|
||
|
||
procedure Fix(var B: Integer);
|
||
begin
|
||
if B < 0 then Exit;
|
||
if Deleted < 0 then B := -1
|
||
else if B = Deleted then B := -1
|
||
else if B > Deleted then Dec(B);
|
||
end;
|
||
|
||
var i, m: Integer;
|
||
begin
|
||
for m := 0 to CFG_MODE_MAX do Fix(FTXProfiles.MainByMode[m]);
|
||
for i := 0 to High(FSlices) do
|
||
if FSlices[i].Active then
|
||
for m := 0 to CFG_MODE_MAX do Fix(FSlices[i].TXProfByMode[m]);
|
||
end;
|
||
|
||
procedure TRadioController.BindTXProfileToCurrentMode(Idx: Integer);
|
||
// Пишет профиль в ячейку «текущий TX-источник + его модуляция». Отдельно от
|
||
// SetSliceTXProfile: там ещё и применение в эфир, а здесь профиль уже применён
|
||
// (нас зовут из SelectTXProfile), повторный ApplyBoundTXProfile был бы циклом.
|
||
var m, Slot: Integer;
|
||
begin
|
||
if (Idx < 0) or (Idx >= FTXProfiles.Count) then Exit;
|
||
m := FlagMode(FTxSliceId);
|
||
if FTxSliceId <= 0 then
|
||
FTXProfiles.MainByMode[m] := Idx // персист — общим блоком в SelectTXProfile
|
||
else
|
||
begin
|
||
Slot := FindSliceIndex(FTxSliceId);
|
||
if Slot >= 0 then FSlices[Slot].TXProfByMode[m] := Idx;
|
||
end;
|
||
end;
|
||
|
||
function TRadioController.FlagMode(SliceId: Integer): Integer;
|
||
// Режим, в котором сейчас стоит этот флаг: у главного — FMode, у слайса — свой.
|
||
var idx: Integer;
|
||
begin
|
||
Result := FMode;
|
||
if SliceId > 0 then
|
||
begin
|
||
idx := FindSliceIndex(SliceId);
|
||
if idx >= 0 then Result := FSlices[idx].Mode;
|
||
end;
|
||
Result := EnsureRange(Result, 0, CFG_MODE_MAX);
|
||
end;
|
||
|
||
function TRadioController.SliceTXProfile(SliceId: Integer): Integer;
|
||
// Привязка «этот источник передачи В ЭТОМ РЕЖИМЕ → этот профиль».
|
||
// -1 = не переключать. Ключ — режим самого флага, поэтому смена модуляции
|
||
// сама достаёт профиль, с которым в этом виде работали в прошлый раз.
|
||
var idx: Integer;
|
||
begin
|
||
Result := -1;
|
||
if SliceId <= 0 then
|
||
Result := FTXProfiles.MainByMode[FlagMode(SliceId)]
|
||
else
|
||
begin
|
||
idx := FindSliceIndex(SliceId);
|
||
if idx >= 0 then Result := FSlices[idx].TXProfByMode[FlagMode(SliceId)];
|
||
end;
|
||
if (Result < 0) or (Result >= FTXProfiles.Count) then Result := -1;
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceTXProfile(SliceId, ProfIdx: Integer);
|
||
// Пишем в ячейку ТЕКУЩЕГО режима этого флага: выбор запоминается за модуляцией.
|
||
var idx, m: Integer;
|
||
begin
|
||
if (ProfIdx < -1) or (ProfIdx >= FTXProfiles.Count) then ProfIdx := -1;
|
||
m := FlagMode(SliceId);
|
||
if SliceId <= 0 then
|
||
begin
|
||
if FTXProfiles.MainByMode[m] = ProfIdx then Exit;
|
||
FTXProfiles.MainByMode[m] := ProfIdx;
|
||
SaveTXProfilesToConfig;
|
||
end
|
||
else
|
||
begin
|
||
idx := FindSliceIndex(SliceId);
|
||
if (idx < 0) or (FSlices[idx].TXProfByMode[m] = ProfIdx) then Exit;
|
||
FSlices[idx].TXProfByMode[m] := ProfIdx;
|
||
// Персист слайсов делает MainForm на общем сохранении панов — здесь только
|
||
// состояние. Привязку главного пишем сразу: она живёт в секции профилей.
|
||
end;
|
||
// Привязали источник, который передаёт прямо сейчас, — применяем немедленно,
|
||
// иначе профиль подхватился бы только при следующей смене TX-источника.
|
||
if SliceId = FTxSliceId then ApplyBoundTXProfile;
|
||
Changed(rfTXProfile);
|
||
end;
|
||
|
||
procedure TRadioController.SetSliceTXProfileTable(SliceId: Integer;
|
||
const T: TTXProfModeTable);
|
||
// Восстановление сохранённой таблицы целиком (персист слайсов/панов).
|
||
var idx, m: Integer;
|
||
begin
|
||
if SliceId <= 0 then
|
||
begin
|
||
FTXProfiles.MainByMode := T;
|
||
Exit;
|
||
end;
|
||
idx := FindSliceIndex(SliceId);
|
||
if idx < 0 then Exit;
|
||
for m := 0 to CFG_MODE_MAX do
|
||
if (T[m] >= 0) and (T[m] < FTXProfiles.Count) then
|
||
FSlices[idx].TXProfByMode[m] := T[m]
|
||
else
|
||
FSlices[idx].TXProfByMode[m] := -1;
|
||
end;
|
||
|
||
function TRadioController.SliceTXProfileTable(SliceId: Integer): TTXProfModeTable;
|
||
var idx: Integer;
|
||
begin
|
||
ClearTXProfModeTable(Result);
|
||
if SliceId <= 0 then Result := FTXProfiles.MainByMode
|
||
else
|
||
begin
|
||
idx := FindSliceIndex(SliceId);
|
||
if idx >= 0 then Result := FSlices[idx].TXProfByMode;
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.ApplyBoundTXProfile;
|
||
// Активный профиль ОДНОЗНАЧНО определяется парой «TX-источник + его модуляция»:
|
||
// ячейка таблицы заполнена → её профиль;
|
||
// ячейка пуста (−1) → базовый профиль (первый в списке).
|
||
// Фолбэк обязателен: без него профиль прошлого режима оставался висеть в новом
|
||
// (симптом «перешёл из FM в USB, а звучит FM»). Правило детерминированное —
|
||
// работает и после перезапуска, в отличие от «вернуть, что было до автоматики».
|
||
var P, M: Integer;
|
||
begin
|
||
if FTXProfiles.Count = 0 then Exit;
|
||
M := FlagMode(FTxSliceId);
|
||
// В DMR передатчика нет, в FM RAW профиль ни на что не влияет
|
||
// (ApplyTXModeSettings глушит там всю обработку) — активный не трогаем вовсе,
|
||
// иначе заход в эти режимы молча перебивал бы выбор оператора.
|
||
if (M = MODE_DMR) or (M = MODE_FMRAW) then Exit;
|
||
P := SliceTXProfile(FTxSliceId);
|
||
if P < 0 then P := 0;
|
||
if P <> FTXProfiles.ActiveIdx then SelectTXProfile(P, True);
|
||
end;
|
||
|
||
procedure TRadioController.ResetTXProfilesToFactory;
|
||
// Пересобирает заводской набор поверх текущего списка. База — ЖИВЫЕ настройки,
|
||
// поэтому «Default» = микрофонный вход, полоса, динамика и мощность как сейчас,
|
||
// но с заводским (ровным) EQ. Единственный способ доехать до обновлённых
|
||
// заводских профилей с уже записанной секцией tx_profiles: она читается, а не
|
||
// пересоздаётся.
|
||
begin
|
||
DefaultTXProfiles(FTXSettings, FDrivePercent, FTXProfiles);
|
||
RemapTXProfileBindings(-1); // список другой — старые привязки бессмысленны
|
||
// ActiveIdx гасим перед Select: иначе StoreActiveTXProfile внутри него
|
||
// впишет в свежий «Default» живой EQ и сброс тембра тут же отменится.
|
||
FTXProfiles.ActiveIdx := -1;
|
||
// Применяем «Default» в эфир — иначе живой EQ остался бы кривым, а профиль
|
||
// говорил бы «ровный», и первая же правка TX-настроек вернула бы кривую.
|
||
SelectTXProfile(0); // сам сохранит и отрендерит
|
||
end;
|
||
|
||
procedure TRadioController.ApplyTXSettingsToDSP;
|
||
begin
|
||
if not FWDSPReady then Exit;
|
||
// Thetis держит CFIR постоянно включённым для openHPSDR Protocol 2:
|
||
// он компенсирует АЧХ FPGA CIC-интерполятора TX. Pluto этого фильтра не
|
||
// имеет, поэтому при смене бэкенда явно выключаем его.
|
||
FDSPEngine.SetTXCFIR(BackendCaps.Kind = bkHPSDR);
|
||
// Mic source — выставляем значение по умолчанию для MOX-кнопки;
|
||
// при реальном PTT SetMOX скорректирует источник.
|
||
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||
FDSPEngine.SetMicGain(FTXSettings.MicGainDB);
|
||
FDSPEngine.SetTXFilterFull(FTXSettings.FilterLow, FTXSettings.FilterHigh,
|
||
FTXSettings.FilterNC, FTXSettings.FilterMP,
|
||
FTXSettings.FilterWindow);
|
||
FDSPEngine.SetTXCompressor(FTXSettings.CompressorOn, FTXSettings.CompressorGain);
|
||
FDSPEngine.SetTXLeveler(FTXSettings.LevelerOn, FTXSettings.LevelerTop,
|
||
FTXSettings.LevelerDecay);
|
||
FDSPEngine.SetTXALC(FTXSettings.ALCOn, FTXSettings.ALCMaxGain,
|
||
FTXSettings.ALCDecay);
|
||
FDSPEngine.SetTXPhaseRot(FTXSettings.PhaseRotOn, FTXSettings.PhaseRotStages,
|
||
FTXSettings.PhaseRotFreq);
|
||
FDSPEngine.SetTXEQ(FTXSettings.EQOn, FTXSettings.EQNumBands,
|
||
FTXSettings.EQGains, FTXSettings.EQFreqs);
|
||
FDSPEngine.SetTXAMCarrierLevel(FTXSettings.AMCarrierLevel);
|
||
FDSPEngine.SetTXFMParams(FTXSettings.FMDeviation, FTXSettings.FMLowCut,
|
||
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
|
||
FDSPEngine.SetTXCTCSS(FTXSettings.CTCSSOn, FTXSettings.CTCSSFreq);
|
||
// TX display
|
||
FDSPEngine.SetTXFFTParams(FTXSettings.TXFFTSize, FTXSettings.TXWindowType);
|
||
FDSPEngine.SetTXSpectrumDisplay(FTXSettings.TXSpecDetector,
|
||
FTXSettings.TXSpecAvgMode,
|
||
FTXSettings.TXSpecAvgTimeMS);
|
||
FDSPEngine.SetTXWaterfallDisplay(FTXSettings.TXWfDetector,
|
||
FTXSettings.TXWfAvgMode,
|
||
FTXSettings.TXWfAvgTimeMS);
|
||
// Reapply the mode overlay last: FM RAW must stay flat even when the user
|
||
// edits and saves ordinary voice-chain settings while this mode is active.
|
||
FDSPEngine.SetTXMode(ActiveTXMode);
|
||
// PureSignal-параметры calcc (mox/loop/tx delay, ptol, pin/map/stbl, SetPk)
|
||
ApplyPSSettingsToDSP;
|
||
end;
|
||
|
||
procedure TRadioController.SetMOX(On_: Boolean);
|
||
var
|
||
WasTransmitting: Boolean;
|
||
begin
|
||
// DMR support is receive-only. This guard also covers CAT, web and hardware
|
||
// PTT. FM RAW is a flat external-modem transmit path.
|
||
if On_ and (ActiveTXMode = MODE_DMR) then Exit;
|
||
// Safety: бэнды с DoNotTx (Alex) и RX-only трансвертер — см. TXProhibited.
|
||
if On_ and TXProhibited then Exit;
|
||
WasTransmitting := FTransmitting;
|
||
// Снятие MOX руками обрывает программную передачу текста: оператор решил
|
||
// прекратить — ключ обязан отпуститься немедленно.
|
||
if (not On_) and CWXBusy then CWXAbort;
|
||
FTransmitting := On_;
|
||
// 2TON: прямое снятие MOX (кнопкой/CAT/web) гасит и двухтональник.
|
||
if (not On_) and FTwoTone then
|
||
begin
|
||
FTwoTone := False;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetTXTwoTone(False, 0, 0);
|
||
Changed(rfTwoTone);
|
||
end;
|
||
if On_ then
|
||
begin
|
||
FDriveLevel := CalcDriveByte;
|
||
ApplyAntPorts; // AD936x: страховка — верный разъём до выхода в эфир
|
||
end;
|
||
FDUCPendingCount := 0; // сброс TX DUC-очереди на смене TX-состояния
|
||
// Сетевая PTT-последовательность TX-on: сначала реле (Alex bit27=1, drive) без
|
||
// PTT, затем PTT=1 — переключаем реле раньше передачи.
|
||
if FTransmitting and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.ClearDUCIQQueue;
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz,
|
||
FDriveLevel, True, True, True, FTuning, FTwoTone);
|
||
FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT
|
||
FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле
|
||
// PureSignal: перевести feedback-DDC0/1 в TX-конфигурацию (частоты = TX,
|
||
// sync, Alex bit18). No-op если PS не вооружён (гейт внутри бэкенда).
|
||
FNetwork.SetPureSignalTX(True);
|
||
// Pre-roll: THROTTLE (2000, порог FPGA — DAC не стартует с пустого буфера)
|
||
// + ДВА WDSP-блока (2×2048) нулевых пар подушкой в очередь. Раньше было 3
|
||
// блока: обоснование «2 не хватало — щелчок через ~0.5с» теперь под вопросом,
|
||
// т.к. тот щелчок по времени (0.5с, не ~21мс) похож не на underrun подушки, а
|
||
// на bfo-глитч тона (устранён коммитом f2d0519). Снижено до 2 блоков ради
|
||
// латентности старта TX (~10мс). Если под нагрузкой DUP / быстром пере-PTT
|
||
// вернётся щелчок РОВНО на старте — это реальный underrun, вернуть 8144.
|
||
// ★При модуляции из TCI 6096 пар (31.75 мс) не хватает: клиент отвечает на
|
||
// первый маркер только через ~33 мс, плюс наша задержка на фронте — радио
|
||
// остаётся без IQ и даёт всплеск на старте КАЖДОЙ передачи. Растягиваем
|
||
// pre-roll до измеренного аванса клиента (в парах @192 кГц = кадры × 4).
|
||
// Путь микрофона радио, CW и web этого не касается: там источник свой.
|
||
if FTCIMicRequested and (FTCITxLeadFrames > 0) then
|
||
FNetwork.PrimeDUCIQ(Max(6096, FTCITxLeadFrames * 4))
|
||
else
|
||
FNetwork.PrimeDUCIQ(6096);
|
||
end;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
// Mic source автоматически по источнику TX. FM RAW always uses the
|
||
// configured default input (normally a virtual PortAudio cable), even if
|
||
// a web client is connected or hardware PTT keyed the transmitter.
|
||
if FTransmitting then
|
||
begin
|
||
if ActiveTXMode = MODE_FMRAW then
|
||
FDSPEngine.SetTXMicSource(DefaultMicSource)
|
||
// TCI впереди web: клиент попросил модуляцию из своего потока ЯВНО
|
||
// (TRX:0,true,tci), а web-микрофон включается самим фактом подключения
|
||
// браузера. Явная просьба сильнее умолчания.
|
||
else if FTCIMicRequested then
|
||
begin
|
||
FTCIMicActive := True;
|
||
FDSPEngine.SetTXMicSource(txmsWeb); // тот же ринг внешней подачи
|
||
end
|
||
else if FWebClientActive then
|
||
begin
|
||
FWebMicActive := True;
|
||
FDSPEngine.SetTXMicSource(txmsWeb);
|
||
end
|
||
else if FHWPTTActive then
|
||
FDSPEngine.SetTXMicSource(txmsRadio)
|
||
else
|
||
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||
end
|
||
else if FWebMicActive or FTCIMicActive then
|
||
begin
|
||
FWebMicActive := False;
|
||
FTCIMicActive := False;
|
||
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||
end;
|
||
// Сброс бэклога sound-card микрофона на RX→TX: пока шёл приём, ринг
|
||
// PortAudio наполнился до упора (~200мс). Без сброса весь этот «старый»
|
||
// звук бёрстом уходит в тракт: стоячая задержка ~190мс в DUC-очереди
|
||
// + в эфир передаётся аудио, записанное ДО нажатия PTT (на TUN оно
|
||
// складывается с тоном поверх полной шкалы → клип-всплеск).
|
||
if FTransmitting and (not WasTransmitting) and Assigned(ActiveMicInput)
|
||
and ActiveMicInput.IsOpen then
|
||
ActiveMicInput.Flush;
|
||
// В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы
|
||
// TX leakage не накапливался в RXA pipeline / FFT-истории анализатора.
|
||
// ★Телеграф — исключение: приёмник должен жить, иначе с ним умолкает и
|
||
// программный сайдтон (он подмешивается в RX-аудио), а при break-in
|
||
// пропадал бы эфир между посылками. От утечки прикрывает ATT on TX.
|
||
FDSPEngine.KeepRXDuringTX := FDisplayDuplex or
|
||
(ActiveTXMode in [MODE_CWL, MODE_CWU]);
|
||
// Мультислайс-TX: передающий тракт под режим слайса-источника (SetMode мог
|
||
// оставить главный режим). Ставим перед стартом TX-потока.
|
||
if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode);
|
||
// PureSignal: mox-флаг calcc (гейтит сбор данных калибровки в pscc).
|
||
FDSPEngine.PSSetMox(FTransmitting);
|
||
// ★Телеграф: голосовой TXA не запускается ВООБЩЕ (эталон Thetis,
|
||
// console.cs:29570). В WDSP режим TXA_CWL — буквально ветка SSB, поэтому
|
||
// без этого гарда MOX в CW отправлял бы в эфир микрофон на 2.9 кГц.
|
||
// Несущую в телеграфе даёт прошивка: кейер, бит CWX или тон TUN.
|
||
// Гард по РЕЖИМУ, а не по CWTXActive: даже с выключенным кейером прошивки
|
||
// голос в телеграфном участке недопустим (тогда MOX = только PTT).
|
||
if FTransmitting and (ActiveTXMode in [MODE_CWL, MODE_CWU])
|
||
and (not FTuning) and (not FTwoTone) then
|
||
FDSPEngine.SetTXRun(False)
|
||
else
|
||
FDSPEngine.SetTXRun(FTransmitting);
|
||
if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then
|
||
begin
|
||
FDSPEngine.FlushRX;
|
||
if Assigned(FAudioOut) then
|
||
FAudioOut.Clear;
|
||
{$IFDEF WINDOWS}
|
||
// Windows: на TX→RX дропаем И RX-IQ, И аудио-выход — radio FPGA TX-buffer +
|
||
// PA slew-down дотравливают «хвост» (на Windows тянется 200–500мс).
|
||
FDSPEngine.BeginPostTXMute(250);
|
||
{$ENDIF}
|
||
end;
|
||
// SetTXRun переключает FActiveDisplayID на TX_DISP_ID; при DUP возвращаем
|
||
// источник на RX, чтобы видеть приём во время передачи.
|
||
if FTransmitting and FDisplayDuplex then
|
||
FDSPEngine.SetDisplaySourceTX(False);
|
||
end;
|
||
// Сетевая PTT-последовательность TX-off: сначала снимаем PTT, затем реле RX.
|
||
if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT
|
||
// PureSignal: вернуть DDC0/1 в RX-конфигурацию (убрать feedback-DDC).
|
||
FNetwork.SetPureSignalTX(False);
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz,
|
||
FDriveLevel, False, True, True, FTuning, FTwoTone);
|
||
FNetwork.ClearDUCIQQueue;
|
||
FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX)
|
||
end;
|
||
ApplyPlutoTxAtten; // Pluto: TX-мощность по drive на передаче, OFF на приёме
|
||
ApplyActiveVolume; // T/R: переключаем громкость RX ↔ TX-монитор (если monitoring)
|
||
// При выключенном break-in локальный кейер вооружён только вместе с MOX
|
||
// (оператор держит T/R руками) — вооружение обязано пойти за этой кнопкой.
|
||
SyncCWKeyer;
|
||
Changed(rfTransmitting);
|
||
end;
|
||
|
||
procedure TRadioController.SetTune(On_: Boolean);
|
||
begin
|
||
if On_ = FTuning then Exit;
|
||
if On_ and (ActiveTXMode in [MODE_DMR, MODE_FMRAW]) then Exit;
|
||
// Safety: те же блокировки, что и в SetMOX — TUN это TX через WDSP PostGen-тон,
|
||
// он тоже должен молчать на бэндах с DoNotTx и на RX-only XVTR.
|
||
if On_ and TXProhibited then Exit;
|
||
if On_ and FTwoTone then SetTwoTone(False); // TUN поверх 2TON — гасим тона
|
||
if On_ then
|
||
begin
|
||
// Тон включаем ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном.
|
||
// В телеграфе тон = pitch: TX-частота уезжает на CWLOOffset (ActiveTXFreqHz),
|
||
// и несущая настройки встаёт РОВНО на VFO, а не на VFO±тон. Так же в Thetis
|
||
// (console.cs:31757, ветка cw_fw_keyer && chkTUN).
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
if ActiveTXMode in [MODE_CWL, MODE_CWU] then
|
||
FDSPEngine.SetTXTone(True, FCWSettings.Pitch, TUN_TONE_MAG)
|
||
else
|
||
FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG);
|
||
end;
|
||
FTuning := True;
|
||
FDriveLevel := CalcDriveByte; // CalcDriveByte увидит FTuning=True (TUN-уровень)
|
||
ApplyWDSPDrive(FTXSettings.TUNLevel);
|
||
if not FTransmitting then SetMOX(True); // PTT + run TXA-канал
|
||
end
|
||
else
|
||
begin
|
||
if FTransmitting then SetMOX(False);
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetTXTone(False, 0, 0);
|
||
FTuning := False;
|
||
FDriveLevel := CalcDriveByte; // вернуть обычный drive%
|
||
ApplyWDSPDrive(FDrivePercent);
|
||
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz,
|
||
FDriveLevel, FTransmitting, True, True, FTuning, FTwoTone);
|
||
FNetwork.SendFullHP;
|
||
end;
|
||
end;
|
||
ApplyPlutoTxAtten; // финальное состояние att по FTuning/FTransmitting (Pluto)
|
||
Changed(rfTuning);
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// Телеграф
|
||
// ===========================================================================
|
||
|
||
function TRadioController.RadioKeyed: Boolean;
|
||
// Выдержка после последней посылки: точка на 20 WPM длится 60 мс, а индикаторы
|
||
// перерисовываются 10 раз в секунду — без неё бейджи и метр мигали бы внутри
|
||
// одного знака. Держим 8 точек, но не меньше 500 мс: на медленной скорости
|
||
// межсловный интервал (7 точек) сам по себе длиннее полусекунды, и фиксированная
|
||
// выдержка гасила бы индикацию между словами.
|
||
var Hold: QWord;
|
||
begin
|
||
Result := FTransmitting;
|
||
if Result then Exit;
|
||
if not CWTXActive then Exit(False);
|
||
if FCWKeyedAt = 0 then Exit(False);
|
||
// Выдержка = hang прошивки + запас, но не меньше 8 точек: полурывок semi-QSK.
|
||
// По ней же переключается реле T/R, поэтому щёлкать оно обязано РАЗ на
|
||
// передачу, а не на каждую посылку.
|
||
Hold := 8 * (1200 div EnsureRange(FCWSettings.Speed, 5, 60));
|
||
if Hold < QWord(FCWSettings.HangTimeMS) + 250 then
|
||
Hold := QWord(FCWSettings.HangTimeMS) + 250;
|
||
if Hold < 500 then Hold := 500;
|
||
Result := (GetTickCount64 - FCWKeyedAt) < Hold;
|
||
end;
|
||
|
||
procedure TRadioController.ServiceCWKeyed;
|
||
// UI про телеграфную передачу иначе не узнает вовсе: SetMOX в телеграфе не
|
||
// вызывается, а значит и rfTransmitting никто не шлёт. Фронт RadioKeyed (с
|
||
// выдержкой) — единственный сигнал «покрасить бейджи и полосы».
|
||
// Зовётся из разбора HP-статуса (openHPSDR, быстро) и с UI-таймера — у Pluto
|
||
// статуса нет вообще, а телеграф там локальный.
|
||
begin
|
||
// Вооружение кейера обязано быть САМОВОССТАНАВЛИВАЮЩИМСЯ: у openHPSDR это
|
||
// делает разбор HP-статуса, а у Pluto статуса нет — значит здесь. Разоружить
|
||
// может что угодно (бэнд, слот трансвертера, выход из TUN), перечислять такие
|
||
// точки поимённо — гарантия однажды пропустить очередную.
|
||
SyncCWKeyer;
|
||
if FCWKeyedShown = RadioKeyed then Exit;
|
||
FCWKeyedShown := RadioKeyed;
|
||
// ★Железная обвязка обязана идти за фактическим ключом: реле T/R (Alex
|
||
// bit 27), TX-антенна, bypass/Ext1 и выходы OC (ими ключуется усилитель)
|
||
// считаются в бэкенде от состояния передачи. Без этого пути прошивка гнала бы
|
||
// несущую, пока Alex стоит в ПРИЁМНОЙ конфигурации. У локального кейера то же
|
||
// делает сессия (CWLocalSession) — синхронно, до первой посылки; здесь остаётся
|
||
// хвост выдержки, когда реле пора отпустить.
|
||
PushNetworkState;
|
||
Changed(rfTransmitting);
|
||
end;
|
||
|
||
function TRadioController.CWSettings: TCWSettings;
|
||
begin
|
||
Result := FCWSettings;
|
||
end;
|
||
|
||
procedure TRadioController.SetCWSettings(const C: TCWSettings);
|
||
// Одна дверь для любой правки телеграфа: pitch уезжает в движок (там он
|
||
// двигает гетеродин и полосу всех CW-каналов), кейер/сайдтон — в прошивку,
|
||
// скорость — в поток программной передачи.
|
||
begin
|
||
FCWSettings := C;
|
||
FCWSettings.Pitch := EnsureRange(FCWSettings.Pitch, 200, 1200);
|
||
FCWSettings.Speed := EnsureRange(FCWSettings.Speed, 5, 60);
|
||
FCWSettings.Weight := EnsureRange(FCWSettings.Weight, 33, 66);
|
||
FCWSettings.SidetoneHWLevel := EnsureRange(FCWSettings.SidetoneHWLevel, 0, 127);
|
||
FCWSettings.SidetoneSWLevel := EnsureRange(FCWSettings.SidetoneSWLevel, 0, 100);
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetCWPitch(FCWSettings.Pitch);
|
||
FDSPEngine.SetCWSpeed(FCWSettings.Speed); // ширина полосы CW на экране
|
||
end;
|
||
if Assigned(FCWSender) then
|
||
FCWSender.SetSpeed(FCWSettings.Speed, FCWSettings.Weight);
|
||
SendDUCSpecificFromSettings;
|
||
// Локальный кейер: параметры + вооружение (галку «кейер в программе», как и
|
||
// запрет передачи, разбирает SyncCWKeyer — одна дверь на всё).
|
||
PushCWLocalConfig;
|
||
SyncCWKeyer;
|
||
// Декодер слушает на том же pitch и стартует адаптацию со своей скорости.
|
||
// Создаём лениво прямо здесь: настройки — единственная дверь, через которую
|
||
// он включается (галка в настройках, кнопка в терминале, загрузка профиля).
|
||
if FCWSettings.Decoder and (not Assigned(FCWDec)) then
|
||
begin
|
||
FCWDec := TCWDecoder.Create;
|
||
FCWDec.SetSpeedHint(FCWSettings.Speed);
|
||
end;
|
||
if Assigned(FCWDec) then
|
||
begin
|
||
FCWDec.SetPitch(FCWSettings.Pitch);
|
||
FCWDec.Enabled := FCWSettings.Decoder;
|
||
end;
|
||
// Тон TUN в телеграфе равен pitch (несущая обязана лечь на VFO, см. SetTune):
|
||
// если крутят pitch прямо на настройке — обновляем живьём.
|
||
if FTuning and FWDSPReady and Assigned(FDSPEngine)
|
||
and (ActiveTXMode in [MODE_CWL, MODE_CWU]) then
|
||
FDSPEngine.SetTXTone(True, FCWSettings.Pitch, TUN_TONE_MAG);
|
||
if FDevConnected and Assigned(FSettings) then
|
||
begin
|
||
FSettings.SaveCW(FDevMAC, FCWSettings);
|
||
FSettings.Save;
|
||
end;
|
||
// Телеграф правят и оператор, и CAT, и TCI, а скорость с задержкой макросов —
|
||
// величины общие для радио: об их смене обязаны узнать все фронтенды, а не
|
||
// только тот, кто её заказал.
|
||
Changed(rfCWSettings);
|
||
end;
|
||
|
||
procedure TRadioController.SyncCWKeyer;
|
||
// Байт 5 зависит от режима ИСТОЧНИКА передачи, поэтому каждая смена режима или
|
||
// TX-слайса обязана его перепослать: вне телеграфа он должен стать нулевым,
|
||
// иначе прошивка поднимет PTT на замыкание ключа посреди SSB.
|
||
var
|
||
WasArmed: Boolean;
|
||
begin
|
||
// Дёшево и идемпотентно: пара сравнений плюс ранний выход внутри
|
||
// SetCWKeyerArmed. Поэтому зовётся не только на смене режима/источника, но и
|
||
// из разбора HP-статуса — вооружение кейера обязано быть САМОВОССТАНАВЛИВАЮЩИМСЯ.
|
||
// Разоружить его может что угодно: заход в RX-only слот трансвертера, переход
|
||
// на бэнд с DoNotTx, правка Alex. Перечислять такие точки поимённо — гарантия
|
||
// однажды пропустить очередную (ровно так уже вышло с DUC-частотой и мощностью).
|
||
if (not (ActiveTXMode in [MODE_CWL, MODE_CWU]))
|
||
and ((Assigned(FCWSender) and FCWSender.Busy)
|
||
or (Assigned(FCWElem) and FCWElem.Busy)) then CWXAbort;
|
||
// Вооружённый кейер = железо может выйти в эфир само ⇒ бэкенд обязан класть
|
||
// уровень мощности в КАЖДЫЙ HP-кадр, а не только на передаче. Локальный
|
||
// генератор — та же история: FTransmitting в телеграфе не поднимается, а
|
||
// несущая идёт.
|
||
if Assigned(FNetwork) then FNetwork.SetCWKeyerArmed(CWTXActive);
|
||
|
||
// ---- Локальный кейер ----------------------------------------------------
|
||
// Вооружение самовосстанавливающееся, как и у прошивки: SyncCWKeyer зовётся
|
||
// из разбора статуса и со всех смен режима/источника/бэнда.
|
||
WasArmed := FCWLocalArmed;
|
||
// Без break-in передачей владеет оператор: ключ имеет право звучать только
|
||
// когда MOX уже поднят — иначе несущая ушла бы в приёмную обвязку.
|
||
// ★TUN и 2TON исключены намеренно: там несущую даёт TXA, и в DUC-очередь уже
|
||
// пишет поток WDSP. Двух продюсеров одной очереди быть не должно.
|
||
FCWLocalArmed := CWTXActive and CWLocalSource
|
||
and FDevConnected and FRunning
|
||
and (FCWSettings.BreakIn or FTransmitting)
|
||
and (not FTuning) and (not FTwoTone);
|
||
if FCWLocalArmed and (not Assigned(FCWLocal)) then EnsureCWLocal;
|
||
if Assigned(FCWLocal) then
|
||
begin
|
||
if FCWLocalArmed then PushCWLocalConfig;
|
||
FCWLocal.SetArmed(FCWLocalArmed);
|
||
end;
|
||
if Assigned(FCWKeyPort) then
|
||
if not FCWLocalArmed then CWKeyPortEvent(False, False);
|
||
// Частота DUC/LO зависит от вооружения (сдвиг несущей) — на фронте
|
||
// перекладываем железо, иначе манипуляция уехала бы от VFO на сдвиг.
|
||
if WasArmed <> FCWLocalArmed then PushNetworkState;
|
||
|
||
if CWOptionsByte <> FLastCWOptions then SendDUCSpecificFromSettings;
|
||
end;
|
||
|
||
procedure TRadioController.EnsureCWLocal;
|
||
// Ленивое создание: у тех, кто телеграфом не пользуется, лишних нитей нет.
|
||
begin
|
||
if not Assigned(FCWLocal) then
|
||
begin
|
||
FCWLocal := TCWLocalKeyer.Create;
|
||
FCWLocal.OnIQ := CWLocalIQ;
|
||
FCWLocal.OnSession := CWLocalSession;
|
||
FCWLocal.OnTick := CWLocalTick;
|
||
end;
|
||
if not Assigned(FCWKeyPort) then
|
||
begin
|
||
FCWKeyPort := TCWKeyPort.Create;
|
||
FCWKeyPort.OnKey := CWKeyPortEvent;
|
||
end;
|
||
PushCWLocalConfig;
|
||
end;
|
||
|
||
procedure TRadioController.PushCWLocalConfig;
|
||
// Настройки → генератор и порт манипулятора. Дёшево и идемпотентно: зовётся из
|
||
// SyncCWKeyer (то есть с каждой смены режима/бэнда) и из SetCWSettings.
|
||
var
|
||
C: TCWLocalConfig;
|
||
P: TCWKeyPortConfig;
|
||
begin
|
||
if not Assigned(FCWLocal) then Exit;
|
||
FillChar(C, SizeOf(C), 0);
|
||
if FWDSPReady and Assigned(FDSPEngine) then C.Rate := FDSPEngine.TXSampleRate
|
||
else C.Rate := TX_SAMPLE_RATE;
|
||
C.OffsetHz := CWCarrierOffsetHz;
|
||
// Уровень несущей. У Pluto мощность задаёт аттенюатор, цифровой уровень
|
||
// держим на максимуме; у openHPSDR цифровой drive работает вместе с
|
||
// байтом 345 — так же, как WDSP-драйв голосового тракта (ApplyWDSPDrive).
|
||
if IsPluto then C.Amplitude := 0.99
|
||
else C.Amplitude := 0.99 * EnsureRange(FDrivePercent, 0, 100) / 100.0;
|
||
C.WPM := FCWSettings.Speed;
|
||
C.Weight := FCWSettings.Weight;
|
||
C.RampMS := FCWSettings.RampMS;
|
||
case FCWSettings.KeyerMode of
|
||
1: C.Mode := clmIambicA;
|
||
2: C.Mode := clmIambicB;
|
||
else C.Mode := clmStraight;
|
||
end;
|
||
C.Reverse := FCWSettings.ReverseKeys;
|
||
C.Strict := FCWSettings.StrictSpacing;
|
||
C.BreakIn := FCWSettings.BreakIn;
|
||
C.HangMS := FCWSettings.HangTimeMS;
|
||
// Реле T/R, LO передатчика (Pluto гасит его на приёме) и аттенюатор успевают
|
||
// встать за это окно. Ноль у прошивки означает «железо знает само», у нас
|
||
// ждать некому — держим минимум.
|
||
C.RFDelayMS := Max(FCWSettings.RFDelayMS, 10);
|
||
C.Sidetone := FCWSettings.SidetoneSW and (FCWSettings.SidetoneSWLevel > 0);
|
||
FCWLocal.Configure(C);
|
||
|
||
if Assigned(FCWKeyPort) then
|
||
begin
|
||
FillChar(P, SizeOf(P), 0);
|
||
P.Enabled := FCWSettings.KeyPortEnabled and (FCWSettings.KeyPort <> '');
|
||
P.Port := FCWSettings.KeyPort;
|
||
P.DotLine := TCWKeyLine(EnsureRange(FCWSettings.KeyDotLine, 0, 3));
|
||
P.DashLine := TCWKeyLine(EnsureRange(FCWSettings.KeyDashLine, 0, 3));
|
||
P.Invert := FCWSettings.KeyInvert;
|
||
P.PowerDTR := FCWSettings.KeyPowerDTR;
|
||
P.PowerRTS := FCWSettings.KeyPowerRTS;
|
||
FCWKeyPort.Configure(P);
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.CWLocalIQ(const Buf: array of Double; Count: Integer);
|
||
// Поток генератора. Тот же маршрут, что у WDSP TXA (OnTXIQ): пары копятся в
|
||
// DUC-пакеты и уходят в бэкенд. Голосовой тракт в телеграфе стоит, так что
|
||
// продюсер тут ровно один.
|
||
begin
|
||
OnTXIQ(Buf, Count);
|
||
end;
|
||
|
||
procedure TRadioController.CWLocalSession(Active: Boolean);
|
||
// Поток генератора. Сессия = одна передача (не одна посылка!): поднимаем PTT,
|
||
// реле T/R с антенной и OC, аттенюатор Pluto — и держим до конца hang.
|
||
// ★FTransmitting НЕ трогаем: в телеграфе передающий тракт WDSP не запускается,
|
||
// а вся индикация идёт через RadioKeyed (см. ApplyHPStatus/ServiceCWKeyed).
|
||
begin
|
||
if Active then FCWKeyedAt := GetTickCount64;
|
||
FCWLocalTX := Active;
|
||
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||
begin
|
||
if Active then
|
||
begin
|
||
PushNetworkState; // RadioKeyed=True: Alex bit27, TX-антенна, OC
|
||
FNetwork.SendPTT(True); // у Pluto здесь же поднимается LO передатчика
|
||
end
|
||
else
|
||
begin
|
||
// ★Если оператор ДЕРЖИТ MOX (ручной T/R поверх break-in), снимать передачу
|
||
// не наше дело — иначе конец посылки выбрасывал бы его из эфира.
|
||
if not FTransmitting then
|
||
begin
|
||
FNetwork.SendPTT(False);
|
||
FNetwork.ClearDUCIQQueue;
|
||
end;
|
||
FCWKeyedAt := GetTickCount64; // выдержка RadioKeyed идёт от КОНЦА сессии
|
||
PushNetworkState;
|
||
end;
|
||
end;
|
||
ApplyPlutoTxAtten;
|
||
// ★Индикацию отсюда НЕ дёргаем: Invoke у GUI — это TThread.Synchronize, то
|
||
// есть блокировка потока генератора до кадра UI. Мало того что она вклинилась
|
||
// бы в путь к эфиру, так ещё и давала бы классический клинч на выходе из
|
||
// программы (UI ждёт WaitFor генератора, генератор — UI). Фронты снимет
|
||
// ServiceCWKeyed с таймера хозяина: для бейджа задержка в кадр незаметна,
|
||
// а выдержка RadioKeyed всё равно не меньше 500 мс.
|
||
end;
|
||
|
||
procedure TRadioController.CWLocalTick;
|
||
// Поток генератора, на каждом блоке (1.25 мс): держим «железо в эфире» живым,
|
||
// иначе выдержка RadioKeyed истекла бы посреди длинного сообщения.
|
||
begin
|
||
FCWKeyedAt := GetTickCount64;
|
||
end;
|
||
|
||
procedure TRadioController.CWKeyPortEvent(Dot, Dash: Boolean);
|
||
// Поток опроса порта. Касание манипулятора обрывает программную передачу —
|
||
// оператор всегда может вклиниться (то же правило, что и у кейера прошивки).
|
||
begin
|
||
if not Assigned(FCWLocal) then Exit;
|
||
if (Dot or Dash) and FCWLocal.Busy then FCWLocal.AbortText;
|
||
FCWLocal.Paddle(Dot, Dash);
|
||
end;
|
||
|
||
procedure TRadioController.CWXKeyEvent(Down: Boolean);
|
||
// Поток передачи, НЕ GUI. Обе операции потокобезопасны: SetCWXKey шлёт HP-пакет
|
||
// под своим локом бэкенда, FCWKeyDown читает DSP-поток.
|
||
begin
|
||
FCWKeyDown := Down;
|
||
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||
FNetwork.SetCWXKey(Down);
|
||
end;
|
||
|
||
function TRadioController.CWXBusy: Boolean;
|
||
begin
|
||
Result := (Assigned(FCWSender) and FCWSender.Busy)
|
||
or (Assigned(FCWLocal) and FCWLocal.Busy)
|
||
or (Assigned(FCWElem) and FCWElem.Busy);
|
||
end;
|
||
|
||
procedure TRadioController.CWElemKey(Down: Boolean);
|
||
// Поток проигрывателя. Чужая манипуляция — это ПРЯМОЙ КЛЮЧ, только с точными
|
||
// длительностями, поэтому и уходит она в те же две двери, что и ключ оператора:
|
||
// у Pluto — во вход прямого ключа локального генератора (он сам поднимает
|
||
// сессию, рисует огибающую и сайдтон), у openHPSDR — в бит CWX прошивки (тем же
|
||
// путём идёт и передача текста).
|
||
begin
|
||
if CWLocalSource then
|
||
begin
|
||
if Assigned(FCWLocal) then FCWLocal.StraightKey(Down);
|
||
Exit;
|
||
end;
|
||
CWXKeyEvent(Down);
|
||
end;
|
||
|
||
procedure TRadioController.CWKeyerElement(Mark: Boolean; Ms: Integer);
|
||
// Поток контроллера (адаптер TCI маршалит через Invoke). Ставим элемент в
|
||
// очередь — играет её отдельный поток по абсолютным дедлайнам.
|
||
begin
|
||
if Ms <= 0 then Exit;
|
||
// Тот же гейт, что у CWXSend: CWTXActive учитывает и режим, и запрет
|
||
// передачи (RX-only слот трансвертера, бэнд с DoNotTx) — там кейер разоружён,
|
||
// и трогать ключ незачем.
|
||
if not CWTXActive then Exit;
|
||
// У Pluto манипуляцию рисуем сами: генератор обязан быть заведён и вооружён,
|
||
// иначе первый же элемент уйдёт в никуда (то же делает CWXSend).
|
||
if CWLocalSource then
|
||
begin
|
||
EnsureCWLocal;
|
||
SyncCWKeyer;
|
||
end;
|
||
if not Assigned(FCWElem) then FCWElem := TCWElemPlayer.Create(Self.CWElemKey);
|
||
FCWElem.Enqueue(Mark, Ms);
|
||
end;
|
||
|
||
procedure TRadioController.CWXSend(const Text: string);
|
||
// Передача текста. Break-in ВКЛ — PTT поднимает прошивка сама (как на ключ);
|
||
// выключен — оператор держит MOX руками, как при ручном T/R.
|
||
begin
|
||
if Text = '' then Exit;
|
||
// CWTXActive, а не просто режим: он же учитывает запрет передачи (RX-only
|
||
// слот трансвертера, бэнд с DoNotTx) — там кейер разоружён, и дёргать бит
|
||
// CWX незачем.
|
||
if not CWTXActive then Exit;
|
||
// Локальный генератор сам держит тайминги В СЭМПЛАХ — текст уходит ему как
|
||
// есть; поток TCWSender (бит CWX прошивки) при этом не нужен вовсе.
|
||
if CWLocalSource then
|
||
begin
|
||
EnsureCWLocal;
|
||
SyncCWKeyer; // вооружиться, если пришли сюда первым делом
|
||
FCWLocal.SendText(Text);
|
||
Exit;
|
||
end;
|
||
if not Assigned(FCWSender) then
|
||
FCWSender := TCWSender.Create(Self.CWXKeyEvent);
|
||
FCWSender.SetSpeed(FCWSettings.Speed, FCWSettings.Weight);
|
||
FCWSender.Enqueue(Text);
|
||
end;
|
||
|
||
procedure TRadioController.CWXAbort;
|
||
begin
|
||
if Assigned(FCWSender) then FCWSender.AbortSending;
|
||
if Assigned(FCWLocal) then FCWLocal.AbortText;
|
||
// Чужая манипуляция обрывается тем же движением: касание манипулятора,
|
||
// снятие MOX и уход из телеграфа гасят её так же, как передачу текста.
|
||
if Assigned(FCWElem) then FCWElem.AbortPlay;
|
||
end;
|
||
|
||
function TRadioController.CWXBackspace: Boolean;
|
||
begin
|
||
if CWLocalSource then
|
||
Result := Assigned(FCWLocal) and FCWLocal.Backspace
|
||
else
|
||
Result := Assigned(FCWSender) and FCWSender.Backspace;
|
||
end;
|
||
|
||
function TRadioController.CWXPendingText: string;
|
||
begin
|
||
if CWLocalSource then
|
||
begin
|
||
if Assigned(FCWLocal) then Result := FCWLocal.PendingText else Result := '';
|
||
end
|
||
else
|
||
begin
|
||
if Assigned(FCWSender) then Result := FCWSender.PendingText else Result := '';
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.CWKeyboardKey(Dot, Dash: Boolean);
|
||
// Манипулятор с клавиатуры. У локального генератора это те же лепестки, что с
|
||
// COM-порта — включая иамбик. У кейера прошивки лепестков нет: их читает FPGA
|
||
// со своего разъёма, а из программы доступен только бит CWX, то есть прямой
|
||
// ключ (это же ограничение у Thetis).
|
||
begin
|
||
if not CWTXActive then
|
||
begin
|
||
// Разоружены (не CW, запрет передачи) — ключ обязан быть отпущен, иначе
|
||
// несущая повиснет при выходе из режима с зажатой клавишей.
|
||
if Assigned(FCWLocal) then FCWLocal.Paddle(False, False);
|
||
CWXKeyEvent(False);
|
||
Exit;
|
||
end;
|
||
if CWLocalSource then
|
||
begin
|
||
EnsureCWLocal;
|
||
SyncCWKeyer;
|
||
FCWLocal.Paddle(Dot, Dash);
|
||
end
|
||
else
|
||
CWXKeyEvent(Dot or Dash);
|
||
end;
|
||
|
||
procedure TRadioController.SetCWDecoder(On_: Boolean);
|
||
begin
|
||
FCWSettings.Decoder := On_;
|
||
if On_ then
|
||
begin
|
||
if not Assigned(FCWDec) then FCWDec := TCWDecoder.Create;
|
||
FCWDec.SetPitch(FCWSettings.Pitch);
|
||
FCWDec.SetSpeedHint(FCWSettings.Speed); // старт адаптации со своей скорости
|
||
FCWDec.Reset;
|
||
FCWDec.Enabled := True;
|
||
end
|
||
else if Assigned(FCWDec) then
|
||
FCWDec.Enabled := False;
|
||
if FDevConnected and Assigned(FSettings) then
|
||
begin
|
||
FSettings.SaveCW(FDevMAC, FCWSettings);
|
||
FSettings.Save;
|
||
end;
|
||
end;
|
||
|
||
function TRadioController.CWDecoderOn: Boolean;
|
||
begin
|
||
Result := Assigned(FCWDec) and FCWDec.Enabled;
|
||
end;
|
||
|
||
procedure TRadioController.ServiceCWDecoder;
|
||
// Поток хозяина (таймер GUI / цикл демона): разбор накопленного аудио и сборка
|
||
// ленты. Здесь же эхо собственной передачи — чтобы в окне была цельная лента
|
||
// связи, а не только принятое.
|
||
var
|
||
S: string;
|
||
|
||
procedure Append(const Txt: string; IsTX: Boolean);
|
||
begin
|
||
if Txt = '' then Exit;
|
||
FCWLogText := FCWLogText + Txt;
|
||
if Length(FCWLogText) > 8192 then
|
||
FCWLogText := Copy(FCWLogText, Length(FCWLogText) - 8191, 8192);
|
||
Inc(FCWLogSeq, Length(Txt));
|
||
if Assigned(FOnCWLog) then FOnCWLog(Txt, IsTX);
|
||
end;
|
||
|
||
begin
|
||
// Эхо передачи забираем всегда: терминал показывает свою работу и при
|
||
// выключенном декодере.
|
||
if Assigned(FCWLocal) then Append(FCWLocal.TakeSentText, True);
|
||
if Assigned(FCWSender) then Append(FCWSender.TakeSentText, True);
|
||
if not Assigned(FCWDec) then Exit;
|
||
if not FCWDec.Enabled then Exit;
|
||
FCWDec.Process;
|
||
S := FCWDec.TakeText;
|
||
Append(S, False);
|
||
end;
|
||
|
||
function TRadioController.CWLogTail(MaxChars: Integer): string;
|
||
begin
|
||
if MaxChars <= 0 then Exit('');
|
||
if Length(FCWLogText) <= MaxChars then Result := FCWLogText
|
||
else Result := Copy(FCWLogText, Length(FCWLogText) - MaxChars + 1, MaxChars);
|
||
end;
|
||
|
||
function TRadioController.CWDecSpeedWPM: Integer;
|
||
begin
|
||
if Assigned(FCWDec) then Result := FCWDec.SpeedWPM else Result := 0;
|
||
end;
|
||
|
||
function TRadioController.CWDecToneOffsetHz: Integer;
|
||
begin
|
||
if Assigned(FCWDec) then Result := FCWDec.ToneOffsetHz else Result := 0;
|
||
end;
|
||
|
||
function TRadioController.CWDecSignal: Boolean;
|
||
begin
|
||
Result := Assigned(FCWDec) and FCWDec.SignalPresent;
|
||
end;
|
||
|
||
function TRadioController.MixSidetone(const Left, Right: array of Single;
|
||
Count: Integer): Boolean;
|
||
// DSP-поток. Генератор синуса на pitch с трапецеидальной огибающей — без неё
|
||
// фронт посылки даёт щелчок в наушниках громче самой посылки. Результат
|
||
// кладём в свои буферы (входные массивы const), вызывающий отдаёт их дальше.
|
||
const
|
||
RAMP_MS = 5;
|
||
AUDIO_SR = 48000;
|
||
var
|
||
i, Got: Integer;
|
||
Amp, Step, Rate, S: Double;
|
||
Local: Boolean;
|
||
begin
|
||
Result := False;
|
||
if not FCWSettings.SidetoneSW then Exit;
|
||
if FCWSettings.SidetoneSWLevel <= 0 then Exit;
|
||
if Count <= 0 then Exit;
|
||
// ★Локальный кейер даёт ГОТОВУЮ огибающую — ту же самую, что ушла в эфир, с
|
||
// точностью до сэмпла. Это снимает старую границу «программный сайдтон верен
|
||
// только для текста и прямого ключа»: при иамбике в прошивке PC видел лишь
|
||
// состояние лепестков, а здесь тайминг его собственный.
|
||
Got := 0;
|
||
Local := FCWLocalArmed and Assigned(FCWLocal);
|
||
if Local then
|
||
begin
|
||
if Length(FCWSTEnvBuf) < Count then SetLength(FCWSTEnvBuf, Count);
|
||
Got := FCWLocal.PullSidetone(FCWSTEnvBuf, Count);
|
||
// Кольцо пусто и хвост уже сошёлся в ноль — блок трогать незачем.
|
||
if (Got <= 0) and (FCWSTEnv <= 0.0) then Exit;
|
||
end
|
||
else if (not FCWKeyDown) and (FCWSTEnv <= 0.0) then
|
||
Exit;
|
||
if Length(FCWSTBufL) < Count then
|
||
begin
|
||
SetLength(FCWSTBufL, Count);
|
||
SetLength(FCWSTBufR, Count);
|
||
end;
|
||
Amp := (FCWSettings.SidetoneSWLevel / 100.0) * 0.7;
|
||
Step := 2.0 * Pi * FCWSettings.Pitch / AUDIO_SR;
|
||
Rate := 1000.0 / (RAMP_MS * AUDIO_SR);
|
||
for i := 0 to Count - 1 do
|
||
begin
|
||
if Local then
|
||
begin
|
||
// Дальше конца кольца доигрываем спадом: генератор либо отстал на
|
||
// блок, либо сессия кончилась.
|
||
if i < Got then FCWSTEnv := FCWSTEnvBuf[i]
|
||
else if FCWSTEnv > 0.0 then
|
||
begin
|
||
FCWSTEnv := FCWSTEnv - Rate;
|
||
if FCWSTEnv < 0.0 then FCWSTEnv := 0.0;
|
||
end;
|
||
end
|
||
else if FCWKeyDown then
|
||
begin
|
||
FCWSTEnv := FCWSTEnv + Rate;
|
||
if FCWSTEnv > 1.0 then FCWSTEnv := 1.0;
|
||
end
|
||
else
|
||
begin
|
||
FCWSTEnv := FCWSTEnv - Rate;
|
||
if FCWSTEnv < 0.0 then FCWSTEnv := 0.0;
|
||
end;
|
||
FCWSTPhase := FCWSTPhase + Step;
|
||
if FCWSTPhase > 2.0 * Pi then FCWSTPhase := FCWSTPhase - 2.0 * Pi;
|
||
S := Amp * FCWSTEnv * Sin(FCWSTPhase);
|
||
FCWSTBufL[i] := EnsureRange(Left[i] + S, -1.0, 1.0);
|
||
FCWSTBufR[i] := EnsureRange(Right[i] + S, -1.0, 1.0);
|
||
end;
|
||
Result := True;
|
||
end;
|
||
|
||
// ===========================================================================
|
||
// PureSignal — оркестрация (порт state machine Thetis PSForm timer1/timer2)
|
||
// ===========================================================================
|
||
|
||
function TRadioController.PSAvailable: Boolean;
|
||
begin
|
||
Result := FDevConnected and BackendCaps.HasPureSignal;
|
||
end;
|
||
|
||
procedure TRadioController.PSSetFeedbackPath(En: Boolean);
|
||
// Единая точка вкл/выкл feedback-тракта: сеть (DDC0/1 на TX) + движок (pscc).
|
||
// Намеренно БЕЗ раннего выхода при том же значении: движок сбрасывает armed
|
||
// при Close (ChangeSampleRate), сеть гейтится внутри — повторный вызов с тем
|
||
// же значением дёшев и восстанавливает рассинхрон.
|
||
begin
|
||
FPSFeedbackOn := En;
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetPureSignalArmed(En);
|
||
if Assigned(FNetwork) then
|
||
begin
|
||
FNetwork.SetPureSignal(En);
|
||
// Включение/выключение посреди передачи — сразу перестроить DDC.
|
||
if FTransmitting then FNetwork.SetPureSignalTX(En);
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.SetPureSignal(On_: Boolean);
|
||
// Кнопка PS = режим авто-калибровки (Thetis AutoCalEnabled). Выключение —
|
||
// через запрос сброса: state machine выполнит reset коррекции и погасит тракт.
|
||
begin
|
||
if On_ = FPSEnabled then Exit;
|
||
if On_ and not PSAvailable then Exit;
|
||
FPSEnabled := On_;
|
||
if On_ then
|
||
begin
|
||
ApplyPSSettingsToDSP; // параметры calcc до старта калибровки
|
||
FPSResetReq := False;
|
||
end
|
||
else
|
||
FPSResetReq := True;
|
||
Changed(rfPureSignal);
|
||
end;
|
||
|
||
procedure TRadioController.PSSingleCal;
|
||
// Одиночная калибровка (Thetis btnPSCalibrate): авто-режим выключается.
|
||
begin
|
||
if not PSAvailable then Exit;
|
||
if FPSSingleCal then begin FPSSingleCal := False; Exit; end; // отмена
|
||
ApplyPSSettingsToDSP;
|
||
FPSEnabled := False;
|
||
FPSSingleCal := True;
|
||
Changed(rfPureSignal);
|
||
end;
|
||
|
||
procedure TRadioController.PSReset;
|
||
begin
|
||
FPSEnabled := False;
|
||
FPSSingleCal := False;
|
||
FPSResetReq := True;
|
||
Changed(rfPureSignal);
|
||
end;
|
||
|
||
function TRadioController.PSFBTarget: Integer;
|
||
begin
|
||
Result := FTXSettings.PSFBTarget;
|
||
if Result <= 0 then
|
||
begin
|
||
if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then
|
||
Result := 22 // Orion MkII: замеры eu2av — при 152 фидбек-АЦП сам даёт IMD
|
||
else
|
||
Result := 152; // классическая цель Thetis
|
||
end;
|
||
end;
|
||
|
||
function TRadioController.PSOutlierSigmaEff: Double;
|
||
begin
|
||
Result := FTXSettings.PSOutlierSigma;
|
||
if Result < 0 then
|
||
begin
|
||
if FDevConnected and (FNetwork <> nil) and (FNetwork.Device.BoardType = 5) then
|
||
Result := 5.0 // Orion MkII: дефолт Thetis-Enhanced eu2av
|
||
else
|
||
Result := 0.0; // выключен
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.ApplyPSSettingsToDSP;
|
||
var
|
||
Ints, Spi: Integer;
|
||
begin
|
||
if not (FWDSPReady and Assigned(FDSPEngine)) then Exit;
|
||
case FTXSettings.PSIntsIdx of
|
||
1: begin Ints := 8; Spi := 512; end;
|
||
2: begin Ints := 4; Spi := 1024; end;
|
||
else begin Ints := 16; Spi := 256; end;
|
||
end;
|
||
FDSPEngine.PSSetParams(FTXSettings.PSMoxDelay, FTXSettings.PSLoopDelay,
|
||
FTXSettings.PSTXDelayNs, FTXSettings.PSRelaxPtol,
|
||
FTXSettings.PSPin, FTXSettings.PSMap,
|
||
FTXSettings.PSStabilize, Ints, Spi,
|
||
FTXSettings.PSHWPeak, PSOutlierSigmaEff);
|
||
// Правка частот двухтональника при активном 2TON — применяем живьём.
|
||
if FTwoTone then
|
||
FDSPEngine.SetTXTwoTone(True, FTXSettings.PSTTFreq1, FTXSettings.PSTTFreq2);
|
||
end;
|
||
|
||
function TRadioController.PSFeedbackLevel: Integer;
|
||
begin
|
||
Result := FPSInfo[4];
|
||
end;
|
||
|
||
function TRadioController.PSCalCount: Integer;
|
||
begin
|
||
Result := FPSInfo[5];
|
||
end;
|
||
|
||
function TRadioController.PSCorrecting: Boolean;
|
||
begin
|
||
Result := FPSInfo[14] = 1;
|
||
end;
|
||
|
||
function TRadioController.PSMaxTXVal: Double;
|
||
begin
|
||
Result := FPSMaxTX;
|
||
end;
|
||
|
||
function TRadioController.PSSingleCalActive: Boolean;
|
||
begin
|
||
Result := FPSSingleCal
|
||
or (FPSCmdState in [PS_ST_TURNON_SINGLE, PS_ST_SINGLECAL]);
|
||
end;
|
||
|
||
procedure TRadioController.PureSignalTick;
|
||
// Вызывается из UI-таймера (~10 Гц). Командная state machine — порт Thetis
|
||
// PSForm timer1code (без Restore-ветки: Save/Restore коррекции не реализованы)
|
||
// + auto-attenuate (timer2code): подстройка AttOnTX по уровню feedback.
|
||
var
|
||
NewAtt: Integer;
|
||
FBTarget: Integer;
|
||
ddB: Double;
|
||
CalAttemptsChanged: Boolean;
|
||
begin
|
||
if not (FWDSPReady and Assigned(FDSPEngine)) then Exit;
|
||
if not PSAvailable then
|
||
begin
|
||
// Устройство сменилось/отключилось: гарантированно всё погасить.
|
||
if FPSFeedbackOn then PSSetFeedbackPath(False);
|
||
FPSCmdState := PS_ST_OFF;
|
||
Exit;
|
||
end;
|
||
// Полный простой (PS ни разу не включался / выключен и сброшен) — не дёргаем
|
||
// WDSP каждые 100 мс впустую.
|
||
if (FPSCmdState = PS_ST_OFF) and not (FPSEnabled or FPSSingleCal
|
||
or FPSResetReq or FPSFeedbackOn) then Exit;
|
||
|
||
// Опрос статуса WDSP calcc
|
||
Move(FPSInfo[0], FPSInfoPrev[0], SizeOf(FPSInfo));
|
||
FDSPEngine.PSGetInfo(FPSInfo);
|
||
FPSMaxTX := FDSPEngine.PSGetMaxTX;
|
||
|
||
// --- Командная state machine ---
|
||
case FPSCmdState of
|
||
PS_ST_OFF:
|
||
begin
|
||
FDSPEngine.PSControl(1, 0, 0, 0);
|
||
PSSetFeedbackPath(False);
|
||
FPSResetReq := False;
|
||
if FPSEnabled then
|
||
FPSCmdState := PS_ST_TURNON_AUTO
|
||
else if FPSSingleCal then
|
||
FPSCmdState := PS_ST_TURNON_SINGLE;
|
||
end;
|
||
PS_ST_TURNON_AUTO:
|
||
begin
|
||
FDSPEngine.PSControl(1, 0, 1, 0);
|
||
PSSetFeedbackPath(True);
|
||
FPSCmdState := PS_ST_AUTOCAL;
|
||
end;
|
||
PS_ST_AUTOCAL:
|
||
begin
|
||
PSSetFeedbackPath(True); // ре-синк (движок мог переоткрыться)
|
||
if FPSResetReq or (not FPSEnabled) then
|
||
FPSCmdState := PS_ST_TURNOFF
|
||
else if FPSSingleCal then
|
||
FPSCmdState := PS_ST_TURNON_SINGLE;
|
||
end;
|
||
PS_ST_TURNON_SINGLE:
|
||
begin
|
||
FDSPEngine.PSControl(1, 1, 0, 0);
|
||
PSSetFeedbackPath(True);
|
||
FPSCmdState := PS_ST_SINGLECAL;
|
||
end;
|
||
PS_ST_SINGLECAL:
|
||
begin
|
||
PSSetFeedbackPath(True); // ре-синк (движок мог переоткрыться)
|
||
FPSSingleCal := False;
|
||
if FPSResetReq then
|
||
FPSCmdState := PS_ST_TURNOFF
|
||
else if FPSEnabled then
|
||
FPSCmdState := PS_ST_TURNON_AUTO
|
||
else if PSCorrecting then
|
||
FPSCmdState := PS_ST_STAYON;
|
||
end;
|
||
PS_ST_STAYON:
|
||
begin
|
||
// Коррекция применяется, калибровка остановлена, feedback не нужен.
|
||
PSSetFeedbackPath(False);
|
||
if FPSResetReq then
|
||
FPSCmdState := PS_ST_TURNOFF
|
||
else if FPSEnabled then
|
||
FPSCmdState := PS_ST_TURNON_AUTO
|
||
else if FPSSingleCal then
|
||
FPSCmdState := PS_ST_TURNON_SINGLE;
|
||
end;
|
||
PS_ST_TURNOFF:
|
||
begin
|
||
// Reset исполняется внутри pscc — держим feedback-тракт, пока calcc
|
||
// не дойдёт до LRESET (как Thetis: PSEnabled=true в состоянии TurnOFF).
|
||
FDSPEngine.PSControl(1, 0, 0, 0);
|
||
PSSetFeedbackPath(True);
|
||
FPSResetReq := False;
|
||
if FPSEnabled then
|
||
FPSCmdState := PS_ST_TURNON_AUTO
|
||
else if FPSSingleCal then
|
||
FPSCmdState := PS_ST_TURNON_SINGLE
|
||
else if (not PSCorrecting) and (FPSInfo[15] = 0) then // 0 = LRESET
|
||
FPSCmdState := PS_ST_OFF;
|
||
end;
|
||
end;
|
||
|
||
// --- Auto-attenuate (Thetis-Enhanced eu2av timer2code) ---
|
||
// По завершении очередной попытки калибровки: если feedback слишком горячий
|
||
// (>1.5×цели) или слабый при ненулевом атт. (<0.7×цели), подстроить AttOnTX
|
||
// так, чтобы уровень лёг к цели PSFBTarget (22 на Orion MkII — выше вход
|
||
// фидбек-АЦП интермодулирует сам, и PS корректирует по искажённому сигналу).
|
||
FBTarget := PSFBTarget;
|
||
case FPSAAState of
|
||
0: begin
|
||
CalAttemptsChanged := FPSInfo[5] <> FPSInfoPrev[5];
|
||
if FTXSettings.PSAutoAtten and CalAttemptsChanged
|
||
and ((FPSInfo[4] > Round(FBTarget * 1.5)) or
|
||
((FPSInfo[4] < Round(FBTarget * 0.7)) and (FTXSettings.AttOnTX > 0))) then
|
||
begin
|
||
FPSAAState := 1;
|
||
if (FPSInfo[4] > 0) and (FPSInfo[4] <= 256) then
|
||
begin
|
||
ddB := 20.0 * Log10(FPSInfo[4] / FBTarget);
|
||
if ddB < -100.0 then ddB := -100.0;
|
||
if ddB > +100.0 then ddB := +100.0;
|
||
end
|
||
else
|
||
ddB := 31.1;
|
||
FPSAADeltaDB := Round(ddB);
|
||
FPSAASaveAuto := Ord(FPSCmdState = PS_ST_AUTOCAL);
|
||
FPSAASaveSingle := Ord(FPSCmdState = PS_ST_SINGLECAL);
|
||
FDSPEngine.PSControl(1, 0, 0, 0); // всё стоп + reset на время смены
|
||
end;
|
||
end;
|
||
1: begin
|
||
FPSAAState := 2;
|
||
NewAtt := EnsureRange(FTXSettings.AttOnTX + FPSAADeltaDB, 0, 31);
|
||
if NewAtt <> FTXSettings.AttOnTX then
|
||
begin
|
||
FTXSettings.AttOnTX := NewAtt;
|
||
SendDUCSpecificFromSettings; // применить атт. немедленно
|
||
Changed(rfPureSignal); // UI: показать новый ATT
|
||
end;
|
||
end;
|
||
2: begin
|
||
FPSAAState := 0;
|
||
FDSPEngine.PSControl(0, FPSAASaveSingle, FPSAASaveAuto, 0);
|
||
end;
|
||
end;
|
||
|
||
// UI-статус (feedback-лампа/счётчик) обновляем при изменении info
|
||
if not CompareMem(@FPSInfo[0], @FPSInfoPrev[0], SizeOf(FPSInfo)) then
|
||
Changed(rfPureSignal);
|
||
end;
|
||
|
||
procedure TRadioController.SetTwoTone(On_: Boolean);
|
||
// 2TON: двухтональник PostGen + MOX (как Thetis chkTestIMD). Уровень — обычный
|
||
// Drive; тона PSTTFreq1/2 (700/1900 по умолчанию).
|
||
begin
|
||
if On_ = FTwoTone then Exit;
|
||
if On_ and (ActiveTXMode in [MODE_DMR, MODE_FMRAW]) then Exit;
|
||
if On_ and FTuning then Exit; // не поверх TUN-тона
|
||
// Safety: те же блокировки, что SetMOX/SetTune.
|
||
if On_ and TXProhibited then Exit;
|
||
if On_ then
|
||
begin
|
||
// Генератор ДО запуска TX-тракта — первые блоки уже с тонами.
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetTXTwoTone(True, FTXSettings.PSTTFreq1, FTXSettings.PSTTFreq2);
|
||
FTwoTone := True;
|
||
if not FTransmitting then SetMOX(True);
|
||
end
|
||
else
|
||
begin
|
||
FTwoTone := False;
|
||
if FTransmitting then SetMOX(False);
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetTXTwoTone(False, 0, 0);
|
||
end;
|
||
Changed(rfTwoTone);
|
||
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.SetSplit(TxOnB: Boolean);
|
||
// Split: TX-VFO = B (TxOnB) либо A. TX-частота берётся из ActiveTXFreqHz
|
||
// (он смотрит на FSplitTxB), поэтому достаточно протолкнуть HP-кадр в сеть.
|
||
// Рендер VFO-дисплеев и подсветки TX-A/TX-B кнопок — через rfActiveVfo.
|
||
begin
|
||
if FSplitTxB = TxOnB then Exit;
|
||
FSplitTxB := TxOnB;
|
||
ApplyAntPorts; // AD936x: TX-разъём — по диапазону нового TX-VFO
|
||
PushNetworkState;
|
||
Changed(rfActiveVfo);
|
||
end;
|
||
|
||
procedure TRadioController.SetVfoLock(On_: Boolean);
|
||
// VFO Lock: пока включён, относительная перестройка (TuneActiveBy — энкодер VFO
|
||
// панели, CAT UP/DN) игнорируется. Подсветка — индикатор aiVFOLock (#11).
|
||
begin
|
||
if FVfoLock = On_ then Exit;
|
||
FVfoLock := On_;
|
||
Changed(rfVfoLock);
|
||
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.StartStop;
|
||
begin SetRun(not FRunning); end;
|
||
|
||
procedure TRadioController.SetRun(On_: Boolean);
|
||
begin FRunning := On_; Changed(rfRunning); { TODO wiring: логика BtnStartStopClick } end;
|
||
|
||
procedure TRadioController.StartRunning(SpectrumWidth: Integer);
|
||
// «Go-live» последовательность Run=1: General packet, wideband/DDC-конфиг,
|
||
// синхронизация WDSP sample rate, восстановление DSP (mode/volume/filter/NR/AGC),
|
||
// Run=1 → seq-сброс → DDC/DUC Specific + TX-цепь. Рендер run-кнопки/sleep — UI
|
||
// через OnControllerState(rfRunning); статус-бар/FSpecView.AGCTop/resize — обёртка.
|
||
var
|
||
GenPkt: TGeneralPacket;
|
||
begin
|
||
// General packet
|
||
FillChar(GenPkt, SizeOf(GenPkt), 0);
|
||
GenPkt.Command := CMD_GENERAL;
|
||
GenPkt.Flags37 := $08;
|
||
GenPkt.Flags38 := $01;
|
||
GenPkt.PAConfig := $01;
|
||
GenPkt.WBPort[0] := (PORT_WIDEBAND_ADC0 shr 8) and $FF;
|
||
GenPkt.WBPort[1] := PORT_WIDEBAND_ADC0 and $FF;
|
||
GenPkt.WBEnable := IfThen(FShowWideband, 1, 0);
|
||
GenPkt.WBSamplesPerPkt[0] := 512 shr 8;
|
||
GenPkt.WBSamplesPerPkt[1] := 512 and $FF;
|
||
GenPkt.WBSampleSize := 16;
|
||
GenPkt.WBUpdateRate := 70;
|
||
GenPkt.WBPacketsPerFrame := 32;
|
||
if FNetwork.Device.BoardType = 5 then
|
||
GenPkt.AlexEnable := $03
|
||
else
|
||
GenPkt.AlexEnable := $01;
|
||
FNetwork.SendGeneralPacket(GenPkt);
|
||
FNetwork.ConfigureWideband(0, FShowWideband, 512, 16, 70, 32);
|
||
|
||
// ANGELIA=3, ORION=4, ORION2=5, SATURN=10: DDC0/1 — PureSignal, юзерский с 2
|
||
// (тот же список, что DDCBaseIndex бэкенда; Saturn раньше был пропущен).
|
||
if FNetwork.Device.BoardType in [3, 4, 5, 10] then
|
||
FActiveDDC := 2
|
||
else
|
||
FActiveDDC := 0;
|
||
ResetPanDDCMaps; // пул пан-DDC заново от нового FActiveDDC
|
||
|
||
// Синхронизируем samplerate WDSP если нужно
|
||
if FWDSPReady and (FDSPEngine.SampleRate <> FSampleRate) then
|
||
begin
|
||
FWDSPReady := False;
|
||
FDSPEngine.ChangeSampleRate(FSampleRate);
|
||
FWDSPReady := FDSPEngine.Initialized;
|
||
end;
|
||
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetMode(FMode);
|
||
FDSPEngine.SetVolume(FVolume / 100.0);
|
||
ApplyModeFilter;
|
||
ApplyNoiseFilterButtonsToDSP;
|
||
// ChangeSampleRate пересоздаёт WDSP-канал — восстанавливаем все настройки
|
||
FDSPEngine.SetAGCTop(FAGCTop);
|
||
ApplyAGCToEngine; // единый маппинг UI-индекс→enum (не прямой каст)
|
||
end;
|
||
|
||
// Сначала отправляем Run=1 — эмулятор/железо создают ddc_specific_thread
|
||
// (порт 1025) и rx_thread (порт 1035+) только после получения HP Run=1.
|
||
// DDC Specific и DUC Specific отправляем ПОСЛЕ, иначе порты ещё не открыты.
|
||
// Уровень пересчитываем здесь же: с вооружённым кейером он уедет в железо
|
||
// сразу, до всякой передачи, а после конструктора FDriveLevel ещё нулевой.
|
||
FDriveLevel := CalcDriveByte;
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz, FDriveLevel, False, True, True, FTuning, FTwoTone);
|
||
// ★DUC — на TX-ЧАСТОТУ, а не на центр дисплея. Раньше здесь стоял центр, и до
|
||
// первой перестройки VFO (или до первого MOX, который частоту дошлёт сам)
|
||
// передатчик стоял не там, где маркер: при CTUN центр и VFO — разные частоты.
|
||
// С кейером в прошивке это стало слышно в эфире: железо может замкнуть ключ
|
||
// сразу после старта, никакого MOX перед этим не будет.
|
||
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq),
|
||
TXTuneFreqHz, FDriveLevel);
|
||
FRunning := True;
|
||
FRXStartTime := GetTickCount64;
|
||
FRXLastPktTime := 0;
|
||
FRXPacketCount := 0;
|
||
FillChar(FDDCSeqValid, SizeOf(FDDCSeqValid), 0);
|
||
FSeqErrorCount := 0;
|
||
FLastSeqErrorDDC := -1;
|
||
FLastSeqErrorDelta := 0;
|
||
FSeqOkStreak := 0;
|
||
|
||
// DDC и DUC Specific — теперь потоки эмулятора слушают на своих портах.
|
||
// DUC Specific содержит mic-конфигурацию (boost/bias/line/PTT) из FTXSettings.
|
||
FNetwork.ConfigureDDCs(1, FSampleRate div 1000, FMainADCSrc,
|
||
FDitherEnabled, FRandomEnabled);
|
||
// Применяем сохранённое состояние динамика/отправки аудио (мьют byte 1400 bit1).
|
||
FNetwork.SetSpeakerAudio(FSendAudioToRadio);
|
||
// RX hw-gain в бэкенд (Pluto). HPSDR — no-op.
|
||
ApplyRxGainToBackend;
|
||
// Антенные разъёмы AD936x для текущего диапазона/трансвертера. HPSDR — no-op.
|
||
ApplyAntPorts;
|
||
// QO-100 self-monitor: RX-аудио на TX (full-duplex). По умолчанию заглушено.
|
||
ApplyRxMuteToEngine;
|
||
// Тон телеграфа в движок: на момент ConnectDevice канал WDSP мог быть ещё
|
||
// не открыт, а здесь он гарантированно жив.
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetCWPitch(FCWSettings.Pitch);
|
||
FDSPEngine.SetCWSpeed(FCWSettings.Speed);
|
||
end;
|
||
SendDUCSpecificFromSettings;
|
||
SyncCWKeyer; // вооружение кейера + уровень мощности в HP-кадре
|
||
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
|
||
ApplyTXSettingsToDSP;
|
||
ApplyModeFilter;
|
||
|
||
// Принудительно обновляем AGC линии — гарантированно после всех SetAGC вызовов
|
||
if FWDSPReady then
|
||
FDSPEngine.UpdateAGCLines(SpectrumWidth); // ещё не получили ни одного пакета
|
||
|
||
Changed(rfRunning); // UI: caption STOP / MOX.Enabled / InhibitSleep
|
||
end;
|
||
|
||
procedure TRadioController.StopRunning;
|
||
// Graceful-останов RX-потока (Run=0) + сброс TX и RX-телеметрии. Сеть остаётся
|
||
// Connected — Disconnect отдельно (UI-обёртка делает это после persistence).
|
||
// Рендер run-кнопки/sleep — UI через OnControllerState(rfRunning); MOX — rfTransmitting.
|
||
var
|
||
p: Integer;
|
||
begin
|
||
if FRunning then
|
||
begin
|
||
// Персист панов/слайсов ДО их гашения (UI-снимок живого состояния).
|
||
if Assigned(FOnBeforeStop) then FOnBeforeStop;
|
||
// Паны на доп. DDC живут только внутри Run-сессии — гасим до Run=0
|
||
// (3.3 UI пересоздаст по персисту при следующем старте).
|
||
for p := 1 to MAX_PANS - 1 do
|
||
RemovePanDDC(p);
|
||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz, 0, False, True, True, FTuning, FTwoTone);
|
||
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq),
|
||
TXTuneFreqHz, 0);
|
||
FRunning := False;
|
||
FTransmitting := False;
|
||
// ★Локальный кейер обязан разоружиться ЗДЕСЬ: таймер хозяина, который его
|
||
// пересинхронизирует, на остановленном радио уже не работает — иначе
|
||
// манипулятор продолжал бы гонять IQ в закрытую сессию.
|
||
SyncCWKeyer;
|
||
// Стоп при активном TX/TUN: хвост IQ в DUC-очереди не должен пережить
|
||
// остановку (иначе при следующем старте уйдёт в эфир старый тон/голос),
|
||
// а TUN-состояние (PostGen-тон) — вернуться в выключенное.
|
||
FNetwork.ClearDUCIQQueue;
|
||
if FTuning then
|
||
begin
|
||
FTuning := False;
|
||
if FWDSPReady then FDSPEngine.SetTXTone(False, 0, 0);
|
||
Changed(rfTuning); // UI: TUN-кнопка off
|
||
end;
|
||
if FTwoTone then
|
||
begin
|
||
FTwoTone := False;
|
||
if FWDSPReady then FDSPEngine.SetTXTwoTone(False, 0, 0);
|
||
Changed(rfTwoTone); // UI: 2TON-кнопка off
|
||
end;
|
||
// PureSignal: погасить калибровку и feedback-тракт (без сброса коррекции —
|
||
// сеть уже останавливается; state machine стартует заново при след. Run).
|
||
if FPSEnabled or FPSSingleCal or FPSFeedbackOn then
|
||
begin
|
||
FPSEnabled := False;
|
||
FPSSingleCal := False;
|
||
FPSResetReq := False;
|
||
FPSCmdState := PS_ST_OFF;
|
||
FPSAAState := 0;
|
||
PSSetFeedbackPath(False);
|
||
if FWDSPReady then FDSPEngine.PSControl(1, 0, 0, 0);
|
||
Changed(rfPureSignal);
|
||
end;
|
||
if FWDSPReady then FDSPEngine.SetTXRun(False);
|
||
Changed(rfTransmitting); // MOX-кнопка off
|
||
end;
|
||
FRXPacketCount := 0;
|
||
FActiveDDC := 0;
|
||
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE;
|
||
FLastPlutoRSSIdB := TELEMETRY_NONE;
|
||
FLastPlutoGainDb := TELEMETRY_NONE;
|
||
FPlutoTeleTick := 0; FPlutoTempBad := 0;
|
||
Changed(rfRunning); // безусловный сброс run-кнопки/MOX.Enabled/sleep (как старый хвост STOP)
|
||
end;
|
||
|
||
function TRadioController.ResolveDevice(const IP: string): THPSDRDevice;
|
||
// discovered→saved по IP/URI: восстанавливает Kind/URI/Serial/BoardType/MAC,
|
||
// иначе простой HPSDR по IP. См. одноимённую обёртку в GUI (TMainForm).
|
||
var
|
||
i: Integer;
|
||
dd: TDiscoveredDevice;
|
||
sd: TSavedDevice;
|
||
begin
|
||
FillChar(Result, SizeOf(Result), 0);
|
||
Result.Kind := bkHPSDR;
|
||
Result.IPAddress := IP;
|
||
|
||
i := FDeviceStore.FindDiscoveredByIP(IP);
|
||
if i >= 0 then
|
||
begin
|
||
dd := FDeviceStore.Discovered(i);
|
||
Result.Kind := dd.Kind;
|
||
Result.URI := dd.URI;
|
||
Result.Serial := dd.Serial;
|
||
Result.BoardType := dd.BoardType;
|
||
Move(dd.MAC[0], Result.MAC[0], 6);
|
||
Exit;
|
||
end;
|
||
|
||
for i := 0 to FDeviceStore.SavedCount - 1 do
|
||
begin
|
||
sd := FDeviceStore.Saved(i);
|
||
if SameText(sd.IPAddress, IP) or SameText(sd.URI, IP) then
|
||
begin
|
||
Result.Kind := sd.Kind;
|
||
Result.URI := sd.URI;
|
||
Result.Serial := sd.Serial;
|
||
Result.BoardType := sd.BoardType;
|
||
Move(sd.MAC[0], Result.MAC[0], 6); // MAC с прошлого connect — ключ профиля настроек
|
||
Exit;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
function TRadioController.EnsureDeviceMac(var Dev: THPSDRDevice): Boolean;
|
||
// См. комментарий в объявлении. Поиск в сети делаем с временно снятым
|
||
// OnDeviceFound: это служебный пробник, а не пользовательский DISCOVER —
|
||
// список устройств в UI он трогать не должен.
|
||
const
|
||
MAC_PROBE_MS = 700; // хватает на LAN: пакет уходит сразу, дальше только ожидание
|
||
var
|
||
i: Integer;
|
||
sd: TSavedDevice;
|
||
SavedCb: TOnDeviceFound;
|
||
SavedIP: string;
|
||
Found: THPSDRDeviceArray;
|
||
begin
|
||
Result := not MacIsZero(Dev.MAC);
|
||
if Result then Exit;
|
||
|
||
i := FDeviceStore.FindDiscoveredByIP(Dev.IPAddress);
|
||
if (i >= 0) and (not MacIsZero(FDeviceStore.Discovered(i).MAC)) then
|
||
begin
|
||
Move(FDeviceStore.Discovered(i).MAC[0], Dev.MAC[0], 6);
|
||
Exit(True);
|
||
end;
|
||
|
||
i := FDeviceStore.FindSavedByAddr(Dev.IPAddress);
|
||
if i >= 0 then
|
||
begin
|
||
sd := FDeviceStore.Saved(i);
|
||
if not MacIsZero(sd.MAC) then
|
||
begin
|
||
Move(sd.MAC[0], Dev.MAC[0], 6);
|
||
Exit(True);
|
||
end;
|
||
end;
|
||
|
||
SavedCb := FNetwork.OnDeviceFound;
|
||
SavedIP := FNetwork.DirectIP;
|
||
FNetwork.OnDeviceFound := nil;
|
||
FNetwork.DirectIP := Dev.IPAddress; // unicast — достанет радио и вне broadcast-домена
|
||
try
|
||
Found := FNetwork.Discover(MAC_PROBE_MS);
|
||
finally
|
||
FNetwork.OnDeviceFound := SavedCb;
|
||
FNetwork.DirectIP := SavedIP;
|
||
end;
|
||
|
||
for i := 0 to High(Found) do
|
||
if SameText(Found[i].IPAddress, Dev.IPAddress) and (not MacIsZero(Found[i].MAC)) then
|
||
begin
|
||
Move(Found[i].MAC[0], Dev.MAC[0], 6);
|
||
if Dev.BoardType = 0 then Dev.BoardType := Found[i].BoardType;
|
||
Exit(True);
|
||
end;
|
||
end;
|
||
|
||
procedure TRadioController.AddSavedDevice(const AName, AAddr: string);
|
||
// web-dev_add: Pluto по схеме URI ('ip:'/'usb:'/'local:'), иначе HPSDR-IP.
|
||
// Serial при ручном добавлении неизвестен (допишется при connect/probe).
|
||
var
|
||
A: string;
|
||
begin
|
||
A := Trim(AAddr);
|
||
if A = '' then Exit;
|
||
if (Pos('ip:', A) = 1) or (Pos('usb:', A) = 1) or (Pos('local:', A) = 1) then
|
||
FDeviceStore.AddSavedPluto(AName, A, '')
|
||
else
|
||
FDeviceStore.AddSaved(AName, A, 0);
|
||
end;
|
||
|
||
function TRadioController.ConnectDevice(const Dev: THPSDRDevice): Boolean;
|
||
// Подключение + загрузка per-device настроек по MAC в поля контроллера + конфиг
|
||
// движка (DSP display, audio). Возвращает False, если Connect не удался (UI
|
||
// покажет сообщение по FNetwork.LastError). Рендер ВСЕХ device-виджетов — UI
|
||
// через OnControllerState(rfDevice). Перестройку диапазона/VFO (RestoreBand/
|
||
// XVTR/ActiveVfo) делает UI-обёртка ПОСЛЕ (там финальная перестройка через
|
||
// ApplyVfoA + wideband/web-extras). FLoadedGlobal хранит загруженный G_Settings
|
||
// для рендера полей, которых нет в контроллере (FPS/FreqMhzDigits/CAT).
|
||
var
|
||
i: Integer;
|
||
D: THPSDRDevice;
|
||
begin
|
||
D := Dev;
|
||
// Подбираем бэкенд под тип устройства (HPSDR/Pluto) до подключения.
|
||
EnsureBackend(D.Kind);
|
||
// MAC — ключ профиля настроек. Старт по сохранённому IP (AutoStart/CONNECT без
|
||
// дискавери) приносит нулевой MAC, и весь профиль устройства уехал бы в чужую
|
||
// секцию конфига; добираем MAC до Connect.
|
||
if D.Kind = bkHPSDR then EnsureDeviceMac(D);
|
||
Result := FNetwork.Connect(D);
|
||
if not Result then Exit;
|
||
// Узнанный MAC запоминаем за сохранённым устройством — следующий старт по
|
||
// AutoStart возьмёт его без поиска в сети.
|
||
if D.IPAddress <> '' then FDeviceStore.SetSavedMac(D.IPAddress, D.MAC)
|
||
else FDeviceStore.SetSavedMac(D.URI, D.MAC);
|
||
|
||
// --- Загружаем настройки устройства по MAC ---
|
||
Move(FNetwork.Device.MAC[0], FDevMAC[0], 6);
|
||
FDevConnected := True;
|
||
FSettings.LoadDevice(FDevMAC, FLoadedGlobal, FBandCache);
|
||
// TX/Alex/XVTR per-device
|
||
FSettings.LoadTX(FDevMAC, FTXSettings);
|
||
FSettings.LoadCW(FDevMAC, FCWSettings);
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
begin
|
||
FDSPEngine.SetCWPitch(FCWSettings.Pitch);
|
||
FDSPEngine.SetCWSpeed(FCWSettings.Speed);
|
||
end;
|
||
// Декодер приёма — по настройке этого устройства (создаётся лениво).
|
||
if FCWSettings.Decoder and (not Assigned(FCWDec)) then
|
||
begin
|
||
FCWDec := TCWDecoder.Create;
|
||
FCWDec.SetSpeedHint(FCWSettings.Speed);
|
||
end;
|
||
if Assigned(FCWDec) then
|
||
begin
|
||
FCWDec.SetPitch(FCWSettings.Pitch);
|
||
FCWDec.Reset;
|
||
FCWDec.Enabled := FCWSettings.Decoder;
|
||
end;
|
||
FSettings.LoadAlex(FDevMAC, FAlexSettings);
|
||
FSettings.LoadAnt936x(FDevMAC, FAnt936x);
|
||
FNetwork.SetAlexConfig(FAlexSettings);
|
||
FSettings.LoadOC(FDevMAC, FOCSettings);
|
||
FNetwork.SetOCConfig(FOCSettings); // no-op на Pluto (base-класс)
|
||
FSettings.LoadFilters(FDevMAC, FFilterTable);
|
||
ApplyFilterSlot(FFilter); // кромки под таблицу этого устройства
|
||
FSettings.LoadXvtr(FDevMAC, FXvtrSettings);
|
||
FCurrentXvtr := -1;
|
||
ApplyXvtrToNetwork;
|
||
FTXSpecRefLevel := FTXSettings.TXSpecRefLevel;
|
||
FTXSpecRange := FTXSettings.TXSpecRange;
|
||
if FTXSettings.TXSpecGridStep > 0 then
|
||
FTXSpecGridStep := FTXSettings.TXSpecGridStep;
|
||
FVolume := FLoadedGlobal.Volume;
|
||
FTxMonVolume := EnsureRange(FLoadedGlobal.TxMonVolume, 0, 100);
|
||
FActiveVfo := EnsureRange(FLoadedGlobal.ActiveVfo, 0, 1); // битый конфиг не должен дать невалидный VFO
|
||
FPAMaxPower := FLoadedGlobal.PAMaxPower;
|
||
FPlutoTxMaxAttDb := EnsureRange(FLoadedGlobal.PlutoTxMaxAttDb, -50.0, 0.0);
|
||
for i := 0 to CFG_BAND_COUNT - 1 do
|
||
FPABandCal[i] := FLoadedGlobal.PABandCal[i];
|
||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||
FVHFBandCal[i] := FLoadedGlobal.VHFBandCal[i];
|
||
SetCalibration(FLoadedGlobal.Cal);
|
||
// drive% (0..100) — раньше грузился через TrkDrive.Position; теперь напрямую,
|
||
// слайдер выставит rfDevice-рендер. Калиброванный байт считаем после FCurrentBand.
|
||
FDrivePercent := EnsureRange(FLoadedGlobal.DriveLevel, 0, 100);
|
||
// TX-профили. Секции нет (конфиг до этой версии) — строим заводской набор из
|
||
// ТЕКУЩИХ настроек: «Default» повторяет их один в один, звук после
|
||
// обновления не меняется. Активный профиль на старте не применяем — живая
|
||
// секция "tx" уже хранит последнее состояние, оно и есть истина.
|
||
if not FSettings.LoadTXProfiles(FDevMAC, FTXProfiles) then
|
||
DefaultTXProfiles(FTXSettings, FDrivePercent, FTXProfiles);
|
||
// Клампим LastBand — иначе битый/старый конфиг → RestoreBand выйдет по гарду,
|
||
// а последующие SetMode/SetFilterIdx запишут в FBandCache[невалид] за массив.
|
||
FCurrentBand := EnsureRange(FLoadedGlobal.LastBand, 0, CFG_BAND_COUNT - 1);
|
||
FDriveLevel := CalcDriveByte;
|
||
// SampleRate — глобальный, до RestoreBand
|
||
if FLoadedGlobal.SampleRate > 0 then
|
||
begin
|
||
FSampleRate := FLoadedGlobal.SampleRate;
|
||
FSpanHz := FSampleRate;
|
||
end;
|
||
// Pluto: HPSDR-наследие rate (48..1536k, часто <520k) невалидно. Клампим к
|
||
// дефолту бэкенда, если ниже минимума (overlay покажет валидный набор).
|
||
with FNetwork.Caps do
|
||
if (Kind = bkPluto) and (FSampleRate < MinSampleRate) then
|
||
begin
|
||
FSampleRate := 1536000;
|
||
FSpanHz := FSampleRate;
|
||
end;
|
||
FRxGainMode := EnsureRange(FLoadedGlobal.RxGainMode, 0, 3);
|
||
FRxGainDb := EnsureRange(FLoadedGlobal.RxGainDb, 0, 73);
|
||
FNRMode := EnsureRange(FLoadedGlobal.NRMode, 0, 4);
|
||
FNBMode := EnsureRange(FLoadedGlobal.NBMode, 0, 2);
|
||
FSNB := FLoadedGlobal.SNBEnabled;
|
||
FANF := FLoadedGlobal.ANFEnabled;
|
||
FWfAGCEnabled := FLoadedGlobal.WfAGCEnabled;
|
||
FWfNFEnabled := FLoadedGlobal.WfNFEnabled;
|
||
FDitherEnabled := FLoadedGlobal.DitherEnabled;
|
||
FRandomEnabled := FLoadedGlobal.RandomEnabled;
|
||
FSendAudioToRadio := FLoadedGlobal.SendAudioToRadio;
|
||
FRawFMDeviation := EnsureRange(FLoadedGlobal.RawFMDeviation,
|
||
RAW_FM_DEV_MIN, RAW_FM_DEV_MAX);
|
||
if FWDSPReady and Assigned(FDSPEngine) then
|
||
FDSPEngine.SetRawFMDeviation(FRawFMDeviation);
|
||
FWfManualHigh := FLoadedGlobal.WfManualHigh;
|
||
FWfManualLow := FLoadedGlobal.WfManualLow;
|
||
FWfAGCOffset := FLoadedGlobal.WfAGCOffset;
|
||
FWfPalette := EnsureRange(FLoadedGlobal.WfPalette, 0, 2);
|
||
FWfGamma := EnsureRange(FLoadedGlobal.WfGamma, 0.3, 3.0);
|
||
FDisplayPixels := EnsureRange(FLoadedGlobal.DisplayPixels, 64, 4096);
|
||
FWfSpeedDiv := EnsureRange(FLoadedGlobal.WfSpeedDiv, 1, 8);
|
||
FSpecMSAA := EnsureRange(FLoadedGlobal.SpecMSAA, 1, 8);
|
||
FBcnZoomSpanHz := EnsureRange(FLoadedGlobal.BeaconZoomSpanHz, 600.0, 200000.0);
|
||
FBcnZoomOffsetHz := EnsureRange(FLoadedGlobal.BeaconZoomOffsetHz, -500000.0, 500000.0);
|
||
FBcnZoomFollow := FLoadedGlobal.BeaconZoomFollow;
|
||
FBcnZoomAuto := FLoadedGlobal.BeaconZoomAuto;
|
||
if FWDSPReady then FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||
FShowSpectrum := FLoadedGlobal.ShowSpectrum;
|
||
FShowWaterfall := FLoadedGlobal.ShowWaterfall;
|
||
FShowWideband := FLoadedGlobal.ShowWideband;
|
||
FWidebandFill := FLoadedGlobal.WidebandFill;
|
||
FSpectrumFill := FLoadedGlobal.SpectrumFill;
|
||
FDisplayDuplex := FLoadedGlobal.DisplayDuplex;
|
||
FRxMuteOnTx := FLoadedGlobal.RxMuteOnTx;
|
||
// Настройки дисплея/сетки + DSP display + аудио-устройства
|
||
if FLoadedGlobal.FFTSize > 0 then
|
||
begin
|
||
FSpecRefLevel := FLoadedGlobal.SpecRefLevel;
|
||
FSpecRange := FLoadedGlobal.SpecRange;
|
||
if FLoadedGlobal.SpecGridStep > 0 then FSpecGridStep := FLoadedGlobal.SpecGridStep;
|
||
if FWDSPReady then
|
||
begin
|
||
FDSPEngine.SetFFTParams(FLoadedGlobal.FFTSize, FLoadedGlobal.WindowType);
|
||
FDSPEngine.SetSpectrumDisplay(FLoadedGlobal.SpecDetector, FLoadedGlobal.SpecAvgMode,
|
||
FLoadedGlobal.SpecAvgTimeMS);
|
||
FDSPEngine.SetWaterfallDisplay(FLoadedGlobal.WfDetector, FLoadedGlobal.WfAvgMode,
|
||
FLoadedGlobal.WfAvgTimeMS);
|
||
end;
|
||
// Локальная звуковая карта (динамик RX / микрофон TX). В демоне FLocalAudio=
|
||
// False: карту не открываем (иначе headless-хост хватает ALSA-устройство и
|
||
// играет звук). Web-аудио идёт через OnAudioConsume и от FAudioOut не зависит.
|
||
if FLocalAudio then
|
||
begin
|
||
// Аудио устройство вывода
|
||
FAudioOut.Close;
|
||
if FLoadedGlobal.AudioSampleRate > 0 then
|
||
FAudioOut.SampleRate := FLoadedGlobal.AudioSampleRate;
|
||
if FLoadedGlobal.AudioOutDevice <> '' then
|
||
begin
|
||
FAudioOut.DeviceIndex := FAudioOut.FindDeviceByName(FLoadedGlobal.AudioOutDevice);
|
||
FAudioOutDevName := FLoadedGlobal.AudioOutDevice;
|
||
end
|
||
else
|
||
begin
|
||
FAudioOut.DeviceIndex := -1;
|
||
FAudioOutDevName := '';
|
||
end;
|
||
FAudioOut.Open;
|
||
// Аудио устройство ввода (TX mic). MicSource определяется в SetMOX по PTT —
|
||
// здесь только открываем устройство.
|
||
if FLoadedGlobal.AudioInDevice <> '' then
|
||
SetMainMicDevice(FAudioIn.FindDeviceByName(FLoadedGlobal.AudioInDevice),
|
||
FLoadedGlobal.AudioInDevice)
|
||
else
|
||
SetMainMicDevice(-1, '');
|
||
end;
|
||
end;
|
||
|
||
Changed(rfDevice); // UI: полная синхронизация device-виджетов из состояния
|
||
end;
|
||
|
||
function TRadioController.ConnectAndRun(const Dev: THPSDRDevice;
|
||
SpectrumWidth: Integer): Boolean;
|
||
// Headless-оркестрация подключения: Connect+load-by-MAC (ConnectDevice) →
|
||
// перестройка диапазона/XVTR/активного VFO → go-live (StartRunning). Рендер всех
|
||
// device/band/run-виджетов идёт через события (rfDevice/rfBandRestore/rfXvtr/
|
||
// rfActiveVfo/rfRunning). UI-обёртка добавляет лишь статус-строки/AGCTop/resize.
|
||
// Возвращает False, если Connect не удался (тогда bring-up не делается).
|
||
begin
|
||
Result := ConnectDevice(Dev);
|
||
if not Result then Exit;
|
||
|
||
// Перестройка диапазона — команда несёт финальную перестройку по активному VFO.
|
||
RestoreBand(FCurrentBand);
|
||
|
||
// Pluto/VHF: HF-частоты вне диапазона. Если восстановленный VFO ниже минимума
|
||
// бэкенда — ставим дефолт VHF (временно 100 МГц, пока нет VHF band-плана).
|
||
with FNetwork.Caps do
|
||
if (Kind = bkPluto) and (FVfoA < MinFreqHz) then
|
||
begin
|
||
FVfoB := 100000000;
|
||
FCenterFreq := 100000000;
|
||
SetVfoA(100000000); // перестроит дисплей + LO
|
||
end;
|
||
|
||
// Восстанавливаем XVTR-режим, если он был активен при выходе. FCurrentBand уже
|
||
// загружен (HF-диапазон до XVTR) — ActivateXvtr только выставит FVfoA/FCurrentXvtr.
|
||
if (FLoadedGlobal.LastXvtr >= 0) and (FLoadedGlobal.LastXvtr < CFG_XVTR_COUNT)
|
||
and FXvtrSettings.Entries[FLoadedGlobal.LastXvtr].Enabled then
|
||
ActivateXvtr(FLoadedGlobal.LastXvtr);
|
||
|
||
// Активный VFO загружен из настроек; RestoreBand/ActivateXvtr строят только
|
||
// VFO A. Если активен B — перестраиваем приёмник И подсветку на него. Для
|
||
// активного A подсветку освежает UI-обёртка (чистый рендер).
|
||
if FActiveVfo = 1 then
|
||
SetActiveVfo(1);
|
||
|
||
// Go-live Run=1 (General/DDC/DUC + DSP restore); рендер — через rfRunning.
|
||
StartRunning(SpectrumWidth);
|
||
end;
|
||
|
||
procedure TRadioController.StopAndDisconnect;
|
||
// Headless STOP+teardown: graceful Run=0 (StopRunning) + персист настроек
|
||
// устройства/диапазона/глобала + разрыв сети. Персист идёт ДО Disconnect (saves
|
||
// читают живое состояние). Рендер run-кнопки/MOX/sleep — через StopRunning→
|
||
// rfRunning; UI-обёртка добавляет очистку спектра и offline-статус.
|
||
begin
|
||
StopRunning;
|
||
// Сохраняем текущую частоту/настройки чтобы следующий START восстановил их.
|
||
if FDevConnected then
|
||
begin
|
||
SaveCurrentBand;
|
||
SaveGlobalSettings;
|
||
FSettings.SaveAlex(FDevMAC, FAlexSettings);
|
||
FSettings.SaveAnt936x(FDevMAC, FAnt936x);
|
||
FSettings.SaveOC(FDevMAC, FOCSettings);
|
||
FSettings.SaveFilters(FDevMAC, FFilterTable);
|
||
// Активный TX-профиль дописываем живыми значениями (drive/громкость микро-
|
||
// фона ездят мимо диалога настроек) и пишем список целиком.
|
||
StoreActiveTXProfile;
|
||
FSettings.SaveTXProfiles(FDevMAC, FTXProfiles);
|
||
// XVTR LastFreq → актуальная видимая частота в момент остановки.
|
||
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
|
||
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
|
||
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||
end;
|
||
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
|
||
FSettings.Save;
|
||
FNetwork.Disconnect;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Discovery — поиск устройств в фоновом потоке. Найденные приходят через
|
||
// FNetwork.OnDeviceFound (=OnDeviceFound) и копятся в FDeviceStore.Discovered;
|
||
// по завершении без устройств — DiscoverFinishedNone. UI слушает rfDeviceList.
|
||
// ---------------------------------------------------------------------------
|
||
type
|
||
TDiscoverThread = class(TThread)
|
||
private
|
||
FCtrl: TRadioController;
|
||
protected
|
||
procedure Execute; override;
|
||
public
|
||
constructor Create(ACtrl: TRadioController);
|
||
end;
|
||
|
||
constructor TDiscoverThread.Create(ACtrl: TRadioController);
|
||
begin
|
||
FCtrl := ACtrl;
|
||
FreeOnTerminate := True;
|
||
inherited Create(False);
|
||
end;
|
||
|
||
procedure TDiscoverThread.Execute;
|
||
// Сканируем ОБА бэкенда транзиентными экземплярами (независимо от FNetwork —
|
||
// он может быть уже подключён/другого типа). Найденные сигналятся колбэком
|
||
// OnDeviceFound во время Discover() и копятся в store.
|
||
var
|
||
Found: Integer;
|
||
HP: THPSDRNetwork;
|
||
Pluto: TPlutoBackend;
|
||
PDev: TRadioDevice;
|
||
i: Integer;
|
||
begin
|
||
Found := 0;
|
||
|
||
// openHPSDR (UDP broadcast)
|
||
HP := THPSDRNetwork.Create;
|
||
try
|
||
HP.OnDeviceFound := FCtrl.OnDeviceFound;
|
||
Found := Found + Length(HP.Discover(2000));
|
||
finally
|
||
HP.Free;
|
||
end;
|
||
|
||
// Pluto (libiio scan + прямой probe сетевых URI) — если libiio доступна.
|
||
if IIOLoad then
|
||
begin
|
||
Pluto := TPlutoBackend.Create;
|
||
try
|
||
Pluto.OnDeviceFound := FCtrl.OnDeviceFound;
|
||
// USB scan
|
||
Found := Found + Length(Pluto.Discover(1500));
|
||
// Прямой probe сетевых адресов (mDNS недоступен) — из списка фронтенда.
|
||
for i := 0 to High(FCtrl.FPlutoProbeURIs) do
|
||
if Pluto.ProbeURI(FCtrl.FPlutoProbeURIs[i], PDev) then
|
||
Inc(Found);
|
||
finally
|
||
Pluto.Free;
|
||
end;
|
||
end;
|
||
|
||
if Found = 0 then
|
||
FCtrl.Invoke(FCtrl.DiscoverFinishedNone);
|
||
end;
|
||
|
||
procedure TRadioController.EnsureBackend(Kind: TBackendKind);
|
||
// Пересоздаёт FNetwork нужного класса, перенося колбэки данных. No-op если тип
|
||
// уже верный. Старый бэкенд отключается и освобождается.
|
||
var
|
||
NewB: TRadioBackend;
|
||
begin
|
||
if (FNetwork <> nil) and (FNetwork.Caps.Kind = Kind) then Exit;
|
||
|
||
case Kind of
|
||
bkPluto: NewB := TPlutoBackend.Create;
|
||
else NewB := THPSDRNetwork.Create;
|
||
end;
|
||
|
||
if FNetwork <> nil then
|
||
begin
|
||
if FNetwork.Connected then FNetwork.Disconnect;
|
||
NewB.OnDeviceFound := FNetwork.OnDeviceFound;
|
||
NewB.OnDDCIQ := FNetwork.OnDDCIQ;
|
||
NewB.OnMicPacket := FNetwork.OnMicPacket;
|
||
NewB.OnHPStatus := FNetwork.OnHPStatus;
|
||
NewB.OnWideband := FNetwork.OnWideband;
|
||
FreeAndNil(FNetwork);
|
||
end;
|
||
FNetwork := NewB;
|
||
end;
|
||
|
||
function TRadioController.BackendCaps: TBackendCaps;
|
||
begin
|
||
if FNetwork <> nil then Result := FNetwork.Caps
|
||
else FillChar(Result, SizeOf(Result), 0);
|
||
end;
|
||
|
||
function TRadioController.SampleRatePresets: TBackendRateArray;
|
||
begin
|
||
Result := BackendCaps.RatePresets;
|
||
end;
|
||
|
||
function TRadioController.BandPlan: TBandPlanArray;
|
||
begin
|
||
Result := BuildBandPlan(IsPluto);
|
||
end;
|
||
|
||
function TRadioController.IsPluto: Boolean;
|
||
begin
|
||
Result := (FNetwork <> nil) and (FNetwork.Caps.Kind = bkPluto);
|
||
end;
|
||
|
||
function TRadioController.InQO100: Boolean;
|
||
// True — активен full-duplex транспондер (QO-100): Pluto + выбран XVTR-слот с
|
||
// флагом FullDuplexTx. Гейт для UI-элементов, осмысленных только в этом режиме
|
||
// (beacon-lock, RX MUTE self-monitor).
|
||
begin
|
||
Result := IsPluto and (FCurrentXvtr >= 0)
|
||
and FXvtrSettings.Entries[FCurrentXvtr].FullDuplexTx;
|
||
end;
|
||
|
||
function TRadioController.DisplayDuplex: Boolean;
|
||
begin
|
||
Result := FDisplayDuplex;
|
||
end;
|
||
|
||
function TRadioController.BoardDisplayName: string;
|
||
var
|
||
Dev: THPSDRDevice;
|
||
begin
|
||
if FNetwork = nil then Exit('');
|
||
Dev := FNetwork.Device;
|
||
if Dev.Kind = bkPluto then
|
||
begin
|
||
Result := PlutoModelName(Dev.Model);
|
||
// Серийник дописываем, только если это реальный hw_serial, а не URI-fallback
|
||
// ('usb:1.5.5' / 'ip:…'), который PlutoBackend кладёт при отсутствии серийника.
|
||
if (Dev.Serial <> '') and (Pos('usb:', Dev.Serial) = 0)
|
||
and (Pos('ip:', Dev.Serial) = 0) then
|
||
Result := Result + ' SN:' + Dev.Serial;
|
||
end
|
||
else
|
||
Result := BoardTypeName(Dev.BoardType);
|
||
end;
|
||
|
||
procedure TRadioController.ServicePlutoTelemetry;
|
||
const
|
||
POLL_MS = 500; // опрос ~2 Гц: чтение attr — USB round-trip, чаще ни к чему
|
||
TEMP_MIN_C = 0.0; // работающий AD936x всегда теплее окружающей (само-нагрев)
|
||
TEMP_MAX_C = 120.0; // абсолютный потолок (датчик исправен)
|
||
TEMP_STEP_C = 12.0; // макс. физичный шаг между опросами (тепловая инерция чипа)
|
||
TEMP_FORCE = 3; // принять «выброс», если держится N раз подряд (не залипаем)
|
||
var
|
||
t, s, r: Double;
|
||
now_: QWord;
|
||
begin
|
||
if (FNetwork = nil) or (not FRunning) or (FNetwork.Caps.Kind <> bkPluto) then Exit;
|
||
now_ := GetTickCount64;
|
||
if (FPlutoTeleTick <> 0) and (now_ - FPlutoTeleTick < POLL_MS) then Exit;
|
||
FPlutoTeleTick := now_;
|
||
if not FNetwork.ReadTelemetry(t, s, r) then Exit;
|
||
|
||
// Температура: датчик AD9361 иногда отдаёт мусор (мид-конверсия / контеншн на
|
||
// USB-контексте с RX-потоком) — наблюдалось редкое -15 °C. Фильтруем: жёсткое
|
||
// окно правдоподобия + slew-limit по тепловой инерции; устойчивый «выброс»
|
||
// (реальный скачок) всё же принимаем через TEMP_FORCE опросов, чтобы не залипнуть.
|
||
if t > TELEMETRY_NONE then
|
||
begin
|
||
if (t >= TEMP_MIN_C) and (t <= TEMP_MAX_C) then
|
||
begin
|
||
if (FLastPlutoTempC <= TELEMETRY_NONE) or (Abs(t - FLastPlutoTempC) <= TEMP_STEP_C) then
|
||
begin
|
||
FLastPlutoTempC := t; FPlutoTempBad := 0;
|
||
end
|
||
else
|
||
begin
|
||
Inc(FPlutoTempBad);
|
||
if FPlutoTempBad >= TEMP_FORCE then
|
||
begin FLastPlutoTempC := t; FPlutoTempBad := 0; end;
|
||
end;
|
||
end;
|
||
// вне окна правдоподобия — молча отбрасываем, держим прошлое валидное значение
|
||
end;
|
||
|
||
// Температура Zynq SoC (xadc): стабильный локальный датчик (не подвержен мусору
|
||
// мид-конверсии AD9361) — достаточно окна правдоподобия без slew-limit.
|
||
if (s > TELEMETRY_NONE) and (s >= TEMP_MIN_C) and (s <= TEMP_MAX_C) then
|
||
FLastPlutoSocTempC := s;
|
||
|
||
// RSSI быстро меняется с сигналом — без сглаживания; обновляем, только если валиден.
|
||
if r > TELEMETRY_NONE then FLastPlutoRSSIdB := r;
|
||
// Фактическое усиление тракта (в MANUAL = заданное, в AGC — снятое с железа).
|
||
FLastPlutoGainDb := FNetwork.RxGainNowDb;
|
||
Changed(rfRxGain); // UI: живая цифра усиления в блоке RF AGC
|
||
|
||
Changed(rfSupply); // поле телеметрии статус-бара (поле 3); web-адаптер тоже
|
||
end;
|
||
|
||
procedure TRadioController.SetPlutoProbeURIs(const URIs: array of string);
|
||
var i: Integer;
|
||
begin
|
||
SetLength(FPlutoProbeURIs, Length(URIs));
|
||
for i := 0 to High(URIs) do FPlutoProbeURIs[i] := URIs[i];
|
||
end;
|
||
|
||
procedure TRadioController.SeedPlutoProbeFromSaved;
|
||
// Заполняет probe-список URI всех сохранённых Pluto (mDNS нет → сетевой Pluto
|
||
// находится только прямым probe его URI). Демон зовёт это перед Discover; GUI
|
||
// добавляет ещё и ручной IP из диалога. Дедупликация по URI.
|
||
var
|
||
URIs: array of string;
|
||
i, k: Integer;
|
||
U: string;
|
||
begin
|
||
SetLength(URIs, 0);
|
||
for i := 0 to FDeviceStore.SavedCount - 1 do
|
||
if FDeviceStore.Saved(i).Kind = bkPluto then
|
||
begin
|
||
U := FDeviceStore.Saved(i).URI;
|
||
if U = '' then Continue;
|
||
for k := 0 to High(URIs) do
|
||
if SameText(URIs[k], U) then begin U := ''; Break; end;
|
||
if U = '' then Continue;
|
||
SetLength(URIs, Length(URIs) + 1);
|
||
URIs[High(URIs)] := U;
|
||
end;
|
||
SetPlutoProbeURIs(URIs);
|
||
end;
|
||
|
||
procedure TRadioController.OnDeviceFound(const Dev: THPSDRDevice);
|
||
// Сетевой/scan-поток: строим строку для списка → pending → маршалинг в
|
||
// ApplyDeviceFound. Строка зависит от типа бэкенда (HPSDR vs Pluto).
|
||
begin
|
||
FPendingFoundDev := Dev;
|
||
if Dev.Kind = bkPluto then
|
||
FPendingFoundEntry := Format('PlutoSDR %s', [Dev.URI])
|
||
else
|
||
FPendingFoundEntry := Format('%s %s FW:%d DDC:%d',
|
||
[Dev.IPAddress, BoardTypeName(Dev.BoardType),
|
||
Dev.FirmwareVersion, Dev.NumDDCs]);
|
||
Invoke(ApplyDeviceFound);
|
||
end;
|
||
|
||
procedure TRadioController.ApplyDeviceFound;
|
||
// Маршалится из OnDeviceFound (Synchronize). Дописываем в store и уведомляем UI.
|
||
begin
|
||
FDiscoverNoneFound := False;
|
||
FDeviceStore.AddDiscovered(FPendingFoundDev.IPAddress, FPendingFoundEntry,
|
||
FPendingFoundDev.BoardType, FPendingFoundDev.MAC,
|
||
FPendingFoundDev.Kind, FPendingFoundDev.URI,
|
||
FPendingFoundDev.Serial);
|
||
Changed(rfDeviceList);
|
||
end;
|
||
|
||
procedure TRadioController.DiscoverFinishedNone;
|
||
// Маршалится из discover-потока, когда устройств не найдено.
|
||
begin
|
||
FDiscoverNoneFound := True;
|
||
Changed(rfDeviceList);
|
||
end;
|
||
|
||
procedure TRadioController.Discover;
|
||
// Старт поиска: чистим прошлый список, сбрасываем direct-IP, запускаем поток.
|
||
begin
|
||
FDeviceStore.ClearDiscovered;
|
||
FDiscoverNoneFound := False;
|
||
FNetwork.DirectIP := '';
|
||
Changed(rfDeviceList);
|
||
TDiscoverThread.Create(Self);
|
||
end;
|
||
|
||
end.
|