Files
ewsdr/ewsdrd.lpr
T
ew8bakandClaude Opus 5 d2096976df fix(tci): всплески на передаче — TX-аудио просилось общим тиком 20 мс
Посреди передачи из MSHV на водопаде появлялись всплески своего сигнала.
Цепочка: очередь DUC пустеет дольше подушки отправителя (DUC_FIFO_THROTTLE
= 2000 отсчётов = 10.4 мс) → FIFO радио сохнет → модуляция обрывается → в
эфире остаётся голая несущая на частоте гетеродина DUC, в стороне от тона
ровно на звуковой сдвиг. Доказано pcap-съёмом: шесть всплесков в дампе —
ровно столько, сколько видел оператор, и каждый стоит за паузой 10.3-23.5 мс,
а паузы 8 мс и короче не дали ни одного.

Виноват не клиент и не блокировка UI, а зернистость НАШЕГО запроса. Слой
первый: PushTxChrono жил на общем тике сервера 20 мс, а просил блок клиента
целиком (2048 отсчётов = 42.7 мс) — маркер выходил через два или три тика,
то есть через 40 или 60 мс. Слой второй: MSHV отвечает пачками по 4-5 блоков
раз в ~44 мс (STREAM_C = 4096 при 96 кГц), и мелкий квант этого не лечит —
нужен запас не меньше пачки.

Сделано:
* квант запроса = один блок TXA (512 отсчётов движка), а не блок клиента;
* свой поток-планировщик TxTickLoop с АБСОЛЮТНЫМИ дедлайнами (опоздание
  одного пробуждения не сдвигает сетку); общий тик маркеров больше не шлёт;
* бухгалтерия Owed/InFlight в кадрах на канал, гасится по k до интерполятора;
  потолок долга обязан быть выше окна в полёте (TCI_TX_OWED_HEADROOM_Q),
  иначе связывающим становится он и подача падает до 58% реального времени
  при полностью исправном клиенте;
* SendBinNow: маркеры пишутся в сокет напрямую под FWriteLock, минуя очередь
  (та выпускается лишь на пробуждении потока клиента, TCI_POLL_MS = 20 мс —
  вдвое больше кванта, и подача снова рвалась);
* FReapLock: планировщик TX — новый поток, а правило «клиента освобождает
  только тик-поток» держалось на том, что им же он и пользуется;
* аванс под зернистость клиента: измеряется по ПЕРИОДУ между пачками (размер
  пачки зависит от того, сколько мы запросили ⇒ положительная обратная связь),
  переживает конец передачи, умеет уменьшаться по выдержке TCI_TX_LEAD_DOWN_MS,
  потолок TCI_TX_LEAD_MAX_MS;
* старт передачи: KickTxTick будит планировщика на фронте PTT, TxPreWarm шлёт
  один маркер ДО SetMOX (41 мс раздумий клиента накладываются на нашу же
  подготовку тракта) под гейтом «передатчик свободен и чужого источника нет»,
  PrimeDUCIQ для источника TCI растянут до Max(6096, аванс×4) — путь микрофона
  радио, CW и web не затронут;
* посев аванса TCI_TX_LEAD_DEF_MS = 50 мс, пока про клиента ничего не известно:
  обучение к первому осушению физически не успевает.

Монотонные часы одного источника для всех потоков — PlatformUtils.MonotonicUs
(абсолютные дедлайны не терпят часов, способных прыгнуть от NTP).

На железе: опасных осушений посреди передачи НОЛЬ (было 12 за 11 с), четыре
передачи из пяти вообще без единого, включая старт; прогон 15:21 чист везде,
в том числе на первой передаче после подключения. Всплесков оператор больше
не видит.

Приборы: TxTrace.pas (EWSDR_TXTRACE=1) и стенд test/hpsdr — кольцевой tcpdump
capture.sh, разбор дампа pcap_tx_scan.py, разбор трассы txtrace_scan.py.
Стенд test/tci: часть F «Пейсинг TX», 260 проверок, провалов нет; живой клиент
с рампой, RTT и потерями — test/tci/tx_chrono_bench.py.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gmVQnna1i4EbSZ2VGm6KD
2026-08-23 22:06:10 +03:00

508 lines
21 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
program ewsdrd;
{$IFDEF FPC}{$MODE objfpc}{$H+}{$ENDIF}
{$APPTYPE CONSOLE}
// ===========================================================================
// EWSDR daemon — headless-режим (только Web-управление, без UI/LCL).
//
// Доказывает, что рефакторинг достиг цели Фазы 5: TRadioController + адаптеры
// (Web) работают БЕЗ TMainForm. Граф зависимостей контроллера LCL-free
// (проверено), поэтому здесь нет ни Interfaces, ни Forms, ни виджетсета.
//
// Маршалинг как в GUI: входящие web-команды приходят из потока веб-сервера и
// выполняются в главном потоке через FCtrl.Invoke (=TThread.Synchronize). Чтобы
// очередь Synchronize реально исполнялась без LCL-цикла сообщений, главный поток
// крутит CheckSynchronize() в Run-цикле — там же периодический PushState.
//
// Сборка (использует уже скомпилированные .ppu от GUI-сборки в lib/...):
// fpc -Mdelphi -O3 -Fulib/$(fpc -iTP)-$(fpc -iTO) -k-L/usr/local/lib \
// -obin/ewsdrd ewsdrd.lpr
// (см. build-ewsdrd.sh). libwdsp.dylib должна быть в /usr/local/lib.
//
// Запуск: ./bin/ewsdrd — если есть autostart-устройство (отмеченное в web/GUI),
// подключается сразу; иначе ждёт connect из web-морды (порт из настроек).
// ===========================================================================
uses
{$IFDEF UNIX}cthreads, BaseUnix,{$ENDIF}
Classes, SysUtils, Math,
RadioController, WebServer, WebAdapter, WDSPEngine,
HPSDRNetwork, Settings, WDSP, PlatformUtils, DeviceStore, BoardUtils, TxTrace;
const
PUSH_INTERVAL_MS = 50; // период исходящего web-зеркала (как GUI-таймер)
type
{ THeadlessHost — связывает контроллер, веб-сервер и веб-адаптер без формы. }
THeadlessHost = class
private
FCtrl: TRadioController;
FWeb: TWebServer;
FWebAd: TWebAdapter;
FRunning: Boolean;
// Параметры web-команд (заполняются в WS-потоке, читаются в Sync* главного потока).
FsStr1, FsStr2: string;
FsInt: Integer;
FsBool: Boolean;
// Маршалинг метода в главный поток (очередь Synchronize крутит Run-цикл).
procedure DoInvoke(M: TThreadMethod);
// Контроллер → наружу: device-list/xvtr не входят в периодический PushState.
procedure OnState(Sender: TObject; Field: TRadioField);
// WDSP открываем лениво и синхронно (нет UI — блокировать главный поток ок).
function EnsureWDSP: Boolean;
// Ширина анализатора в демоне: рендера нет, поэтому её задаёт потолок
// web-кадра (web.spec_pixels) — ровно столько точек и уйдёт клиенту.
function DaemonSpecWidth: Integer;
procedure ConnectTo(const Dev: THPSDRDevice);
procedure PushDeviceList;
procedure PushXvtr;
procedure PushRatePresets; // sample-rate пресеты бэкенда → web
procedure PushBands; // band-план бэкенда → web
procedure PushFreqDigits; // per-device freq_mhz_digits → web (формат VFO)
// Входящие web-команды (из WS-потока): запоминаем параметр и маршалим в Sync*.
procedure WebRun(On_: Boolean);
procedure WebConnect(const IP: string);
procedure WebDiscover;
procedure WebDisconnect;
procedure WebDevAdd(const AName, IP: string);
procedure WebDevRemove(Idx: Integer);
procedure WebDevAutoStart(Idx: Integer);
// Sync* — выполняются в главном потоке через DoInvoke.
procedure SyncConnect;
procedure SyncDisconnect;
procedure SyncDiscover;
procedure SyncRun;
procedure SyncDevAdd;
procedure SyncDevRemove;
procedure SyncDevAutoStart;
public
constructor Create;
destructor Destroy; override;
procedure Run;
procedure RequestStop; // из обработчика сигнала (только флаг)
end;
var
GHost: THeadlessHost = nil; // для обработчика сигнала
procedure Log(const S: string);
begin
WriteLn(FormatDateTime('hh:nn:ss', Now), ' ', S);
Flush(Output);
end;
function DaemonAppName: string;
begin
// КРИТИЧНО: тот же каталог конфигурации, что у GUI (~/.config/EWSDR/).
// GetAppConfigDir берёт имя из ApplicationName, иначе бы вышло «ewsdrd» —
// отдельный пустой конфиг (нет device-настроек/FFT-параметров/FFTW-wisdom →
// белый водопад + WDSP открывается ~минуту). GUI задаёт Application.Title:=EWSDR.
Result := 'EWSDR';
end;
{$IFDEF UNIX}
procedure HandleSignal(sig: cint); cdecl;
begin
if Assigned(GHost) then GHost.RequestStop;
end;
{$ENDIF}
{ THeadlessHost }
constructor THeadlessHost.Create;
var
StartupVfoA, StartupVfoB: Double;
StartupRate: Integer;
WebCfg: TWebSettings;
begin
inherited Create;
FCtrl := TRadioController.Create; // создаёт FSettings + FDeviceStore (LoadSaved)
FCtrl.LocalAudioEnabled := False; // headless: звук только в web, не на карту хоста
FCtrl.OnInvoke := @DoInvoke;
FCtrl.OnStateChanged := @OnState;
// Настройки + startup-частота ДО CreateEngines (DSP рождается с верным rate).
FCtrl.FSettings.Load;
if FCtrl.FSettings.LoadStartupPreview(StartupVfoA, StartupVfoB, StartupRate)
and (StartupRate > 0) then
begin
FCtrl.FVfoA := StartupVfoA;
FCtrl.FVfoB := StartupVfoB;
FCtrl.FSampleRate := StartupRate;
FCtrl.FSpanHz := StartupRate;
end;
FCtrl.CreateEngines(FCtrl.FSampleRate);
// Data-path колбэки (сеть/DSP) — все на контроллер (backend-агностично).
FCtrl.FNetwork.OnDeviceFound := @FCtrl.OnDeviceFound;
FCtrl.FNetwork.OnHPStatus := @FCtrl.OnHPStatus;
FCtrl.FNetwork.OnDDCIQ := @FCtrl.OnDDCIQ;
FCtrl.FNetwork.OnMicPacket := @FCtrl.OnMicPacket;
// OnWideband пропущен — это дисплей-панорама, демону не нужна.
FCtrl.FDSPEngine.OnAudio := @FCtrl.OnAudioReady;
FCtrl.FDSPEngine.OnSpectrum := @FCtrl.OnSpectrumReady;
FCtrl.FDSPEngine.OnWaterfall := @FCtrl.OnWaterfallReady;
FCtrl.FDSPEngine.OnTXIQ := @FCtrl.OnTXIQ;
FCtrl.FDSPEngine.OnPullMicSamples := @FCtrl.PullSoundCardMic;
// OnSpectrumData/OnWaterfallData НЕ навешиваем — рендера нет; web читает кадр
// напрямую из FCtrl.FSpectrumBuf в PushState.
// --- Веб-сервер + адаптер ---
FCtrl.FSettings.LoadWebSettings(WebCfg);
FCtrl.FWebSpecPixels := EnsureRange(WebCfg.SpecPixels, 256, SPECTRUM_PIXELS);
FWeb := TWebServer.Create(WebCfg.User, WebCfg.Pass, Word(WebCfg.Port), WebCfg.BindAddr);
FWebAd := TWebAdapter.Create(FCtrl, FWeb, nil);
// RX-аудио веб-клиенту — через route-хук контроллера (D1).
FCtrl.OnAudioConsume := @FWebAd.AudioConsume;
// Чистые контроллер-команды → адаптер (как в GUI).
FWeb.OnMode := @FWebAd.OnMode; FWeb.OnFilter := @FWebAd.OnFilter;
FWeb.OnAGC := @FWebAd.OnAGC; FWeb.OnAGCTop := @FWebAd.OnAGCTop;
FWeb.OnVolume := @FWebAd.OnVolume; FWeb.OnWfAGC := @FWebAd.OnWfAGC;
FWeb.OnWfNF := @FWebAd.OnWfNF; FWeb.OnMute := @FWebAd.OnMute;
FWeb.OnNR := @FWebAd.OnNR; FWeb.OnNB := @FWebAd.OnNB;
FWeb.OnSNB := @FWebAd.OnSNB; FWeb.OnANF := @FWebAd.OnANF;
FWeb.OnFreqB := @FWebAd.OnFreqB; FWeb.OnCenter := @FWebAd.OnCenter;
FWeb.OnAttn := @FWebAd.OnAttn; FWeb.OnFreq := @FWebAd.OnFreq;
FWeb.OnFreqA := @FWebAd.OnFreqA; FWeb.OnBand := @FWebAd.OnBand;
FWeb.OnSpan := @FWebAd.OnSpan; FWeb.OnDrive := @FWebAd.OnDrive;
FWeb.OnCtun := @FWebAd.OnCtun; FWeb.OnActiveVfo := @FWebAd.OnActiveVfo;
FWeb.OnMOX := @FWebAd.OnMOX; FWeb.OnTun := @FWebAd.OnTun;
FWeb.OnFMStep := @FWebAd.OnFMStep; FWeb.OnXvtrBand := @FWebAd.OnXvtrBand;
FWeb.OnBeacon := @FWebAd.OnBeacon; FWeb.OnBeaconSeed := @FWebAd.OnBeaconSeed;
FWeb.OnRxGainMode := @FWebAd.OnRxGainMode;
FWeb.OnRxGain := @FWebAd.OnRxGain; FWeb.OnRxMute := @FWebAd.OnRxMute;
FWeb.OnWebMic := @FWebAd.OnMic;
FWeb.OnClientActiveChanged := @FWebAd.OnClientActiveChanged;
// Host-coupled (lifecycle) — на демон-хост (маршалит в главный поток).
FWeb.OnRun := @WebRun;
FWeb.OnConnect := @WebConnect;
FWeb.OnDiscoverDev := @WebDiscover;
FWeb.OnDisconnectDev := @WebDisconnect;
FWeb.OnDevAdd := @WebDevAdd;
FWeb.OnDevRemove := @WebDevRemove;
FWeb.OnDevAutoStart := @WebDevAutoStart;
end;
destructor THeadlessHost.Destroy;
begin
if Assigned(FWeb) then FWeb.Stop;
FreeAndNil(FWebAd);
FreeAndNil(FWeb);
FreeAndNil(FCtrl); // Destroy зовёт FreeEngines (Disconnect если Connected)
inherited Destroy;
end;
procedure THeadlessHost.DoInvoke(M: TThreadMethod);
begin
TThread.Synchronize(nil, M);
end;
procedure THeadlessHost.OnState(Sender: TObject; Field: TRadioField);
begin
// Большинство rf* — это UI-рендер (демону не нужен). Сюда попадают только
// выходные web-списки, которых нет в периодическом PushState.
case Field of
rfDeviceList: PushDeviceList; // discovered обновился
rfXvtr, rfDevice: begin PushXvtr; PushDeviceList; PushRatePresets; PushBands; PushFreqDigits; end; // connect / xvtr / backend
end;
end;
function THeadlessHost.EnsureWDSP: Boolean;
var
WisdomDir: AnsiString;
begin
if FCtrl.FWDSPReady then Exit(True);
// FFTW-wisdom ДО Open: иначе XCreateAnalyzer строит планы с нуля (~минуту).
// WDSPwisdom(dir) импортирует готовый wdspWisdom00 (обычно создан GUI) за мс;
// если файла нет — строит и пишет его (одноразово, медленно). Как GUI
// (MainForm.EnsureWDSPWisdom), но без потока/диалога — демону блокировать ок.
if Assigned(@WDSPwisdom) then
begin
WisdomDir := AnsiString(GetAppCfgDir);
if not FileExists(string(WisdomDir) + 'wdspWisdom00') then
Log('Building WDSP wisdom (one-time, may take ~30-60s)...');
WDSPwisdom(PAnsiChar(WisdomDir));
end;
Log('Opening WDSP...');
Result := FCtrl.FDSPEngine.Open;
FCtrl.FWDSPReady := Result;
if not Result then
begin
Log('WDSP open FAILED: ' + FCtrl.FDSPEngine.LastError);
Exit;
end;
// Ширина анализатора спектра. Без UI её никто не выставляет (GUI зовёт
// SetSpectrumWidth(панель) каждый тик), и анализатор отдал бы дефолтные 64
// пикселя. Ставим ровно под потолок web-кадра (web.spec_pixels) — больше
// демону не нужно, меньше — обрезало бы настройку.
// FLastSpectrumW переживает пересоздание канала.
FCtrl.FDSPEngine.SetSpectrumWidth(DaemonSpecWidth);
end;
function THeadlessHost.DaemonSpecWidth: Integer;
begin
Result := EnsureRange(FCtrl.FWebSpecPixels, 256, SPECTRUM_PIXELS);
end;
procedure THeadlessHost.ConnectTo(const Dev: THPSDRDevice);
begin
if FCtrl.FNetwork.Connected then Exit;
if not EnsureWDSP then Exit;
Log('Connecting to ' + Dev.IPAddress + '...');
if FCtrl.ConnectAndRun(Dev, DaemonSpecWidth) then
Log('Running. Board=' + IntToStr(FCtrl.FNetwork.Device.BoardType))
else
Log('Connect FAILED: ' + FCtrl.FNetwork.LastError);
end;
procedure THeadlessHost.PushDeviceList;
var
Arr: TWebDeviceArray;
i, k: Integer;
begin
SetLength(Arr, FCtrl.FDeviceStore.SavedCount + FCtrl.FDeviceStore.DiscoveredCount);
k := 0;
// saved ПЕРВЫМИ — их позиция == saved-индекс (web шлёт его в dev_remove/autostart).
for i := 0 to FCtrl.FDeviceStore.SavedCount - 1 do
begin
Arr[k].Name := FCtrl.FDeviceStore.Saved(i).Name;
Arr[k].IP := FCtrl.FDeviceStore.Saved(i).IPAddress;
Arr[k].BoardType := FCtrl.FDeviceStore.Saved(i).BoardType;
Arr[k].IsSaved := True;
Arr[k].AutoStart := FCtrl.FDeviceStore.Saved(i).AutoStart;
Inc(k);
end;
for i := 0 to FCtrl.FDeviceStore.DiscoveredCount - 1 do
begin
Arr[k].Name := FCtrl.FDeviceStore.Discovered(i).DisplayName;
Arr[k].IP := FCtrl.FDeviceStore.Discovered(i).IPAddress;
Arr[k].BoardType := FCtrl.FDeviceStore.Discovered(i).BoardType;
Arr[k].IsSaved := False;
Arr[k].AutoStart := False;
Inc(k);
end;
FWeb.SetDeviceList(Arr);
end;
procedure THeadlessHost.PushXvtr;
var
Arr: TWebXvtrArray;
i, Cnt: Integer;
begin
Cnt := 0;
for i := 0 to CFG_XVTR_COUNT - 1 do
if FCtrl.FXvtrSettings.Entries[i].Enabled then Inc(Cnt);
SetLength(Arr, Cnt);
Cnt := 0;
for i := 0 to CFG_XVTR_COUNT - 1 do
if FCtrl.FXvtrSettings.Entries[i].Enabled then
begin
Arr[Cnt].Idx := i;
Arr[Cnt].Name := FCtrl.FXvtrSettings.Entries[i].ButtonText;
Inc(Cnt);
end;
FWeb.SetXvtrBands(Arr, FCtrl.FCurrentXvtr);
end;
procedure THeadlessHost.PushRatePresets;
begin
FWeb.SetRatePresets(FCtrl.SampleRatePresets);
end;
procedure THeadlessHost.PushBands;
var
Plan: TBandPlanArray;
Names: array of string;
Freqs: array of Double;
i: Integer;
begin
Plan := FCtrl.BandPlan;
SetLength(Names, Length(Plan));
SetLength(Freqs, Length(Plan));
for i := 0 to High(Plan) do
begin
Names[i] := Plan[i].Name;
Freqs[i] := Plan[i].FreqHz;
end;
FWeb.SetBands(Names, Freqs);
with FCtrl.BackendCaps do FWeb.SetFreqRange(MinFreqHz, MaxFreqHz);
end;
procedure THeadlessHost.PushFreqDigits;
// Зеркалим per-device настройку формата VFO в web (как GUI ApplyFreqMhzDigits).
// Без этого демон всегда слал бы дефолтные 3 цифры (999 МГц), и VHF/UHF/SHF не влезал.
var d: Integer;
begin
d := FCtrl.FLoadedGlobal.FreqMhzDigits;
if d < 3 then d := 3 else if d > 5 then d := 5;
FWeb.FreqMhzDigits := d;
end;
// --- Входящие web-команды (WS-поток) → маршалинг в главный поток ---
procedure THeadlessHost.WebRun(On_: Boolean);
begin FsBool := On_; FCtrl.Invoke(@SyncRun); end;
procedure THeadlessHost.WebConnect(const IP: string);
begin FsStr1 := IP; FCtrl.Invoke(@SyncConnect); end;
procedure THeadlessHost.WebDiscover;
begin FCtrl.Invoke(@SyncDiscover); end;
procedure THeadlessHost.WebDisconnect;
begin FCtrl.Invoke(@SyncDisconnect); end;
procedure THeadlessHost.WebDevAdd(const AName, IP: string);
begin FsStr1 := AName; FsStr2 := IP; FCtrl.Invoke(@SyncDevAdd); end;
procedure THeadlessHost.WebDevRemove(Idx: Integer);
begin FsInt := Idx; FCtrl.Invoke(@SyncDevRemove); end;
procedure THeadlessHost.WebDevAutoStart(Idx: Integer);
begin FsInt := Idx; FCtrl.Invoke(@SyncDevAutoStart); end;
// --- Sync* (главный поток) ---
procedure THeadlessHost.SyncConnect;
begin
if FCtrl.FNetwork.Connected then Exit;
// Kind/URI/Serial/BoardType при Connect не приходят — восстанавливаем из store
// (discovered→saved). Без Kind/URI бэкенд бы выбрался HPSDR, и Pluto не открылся.
ConnectTo(FCtrl.ResolveDevice(FsStr1));
end;
procedure THeadlessHost.SyncDisconnect;
begin
if FCtrl.FNetwork.Connected then
begin
FCtrl.StopAndDisconnect;
Log('Disconnected (web).');
end;
end;
procedure THeadlessHost.SyncDiscover;
begin
// Сетевой Pluto без mDNS находится только прямым probe URI — сидируем список из
// сохранённых Pluto (в GUI это делает BuildProbeURIs). Без этого Discover видит
// лишь HPSDR-broadcast и USB-Pluto, а сетевые Pluto — никогда.
FCtrl.SeedPlutoProbeFromSaved;
FCtrl.Discover;
end;
procedure THeadlessHost.SyncRun;
begin
// set_run:true бессмыслен без выбранного устройства (web стартует через connect);
// здесь только STOP-путь, симметрично web-disconnect.
if (not FsBool) and FCtrl.FNetwork.Connected then
begin
FCtrl.StopAndDisconnect;
Log('Stopped (web set_run:false).');
end;
end;
procedure THeadlessHost.SyncDevAdd;
begin FCtrl.AddSavedDevice(FsStr1, FsStr2); PushDeviceList; end;
procedure THeadlessHost.SyncDevRemove;
begin FCtrl.FDeviceStore.RemoveSaved(FsInt); PushDeviceList; end;
procedure THeadlessHost.SyncDevAutoStart;
begin FCtrl.FDeviceStore.SetAutoStart(FsInt); PushDeviceList; end;
procedure THeadlessHost.RequestStop;
begin
FRunning := False;
end;
procedure THeadlessHost.Run;
var
WebCfg: TWebSettings;
Dev: THPSDRDevice;
AutoDev: TSavedDevice;
lastPush, lastBeacon, nowMs: QWord;
begin
FCtrl.FSettings.LoadWebSettings(WebCfg);
if not FWeb.Start then
begin
Log('Web server FAILED to start on ' + WebCfg.BindAddr + ':' + IntToStr(WebCfg.Port));
Exit;
end;
Log('Web control on http://' + WebCfg.BindAddr + ':' + IntToStr(WebCfg.Port));
PushDeviceList;
PushRatePresets; // дефолтный бэкенд (HPSDR) до connect; перезапишется на смене
PushBands;
// FRunning ставим ДО autostart: первый WDSP-open может занять время, и если за
// него прилетит SIGINT (RequestStop→FRunning:=False), цикл сразу завершится, а
// не перетрётся обратно в True.
FRunning := True;
// Autostart — подключаемся сразу к отмеченному устройству. Берём полную запись
// (Kind/URI/Serial), иначе Pluto не подключить: бэкенд выбирается по Kind, USB-
// Pluto открывается по URI ('usb:..') и не имеет IPAddress.
if FCtrl.FDeviceStore.AutoStartDevice(AutoDev) then
begin
FillChar(Dev, SizeOf(Dev), 0);
Dev.Kind := AutoDev.Kind;
Dev.IPAddress := AutoDev.IPAddress;
Dev.URI := AutoDev.URI;
Dev.Serial := AutoDev.Serial;
Dev.BoardType := AutoDev.BoardType;
ConnectTo(Dev);
end
else
Log('No autostart device — waiting for web connect.');
lastPush := GetTickCount64;
lastBeacon := lastPush;
while FRunning do
begin
CheckSynchronize(PUSH_INTERVAL_MS); // исполняет очередь Invoke + ждёт до 50мс
nowMs := GetTickCount64;
// QO-100 beacon lock — одна итерация контура @~10 Гц (как GUI MeterTimer). No-op
// если лок выключен. Без этого лок маяка не работал бы в headless-демоне.
if nowMs - lastBeacon >= 100 then
begin
if FCtrl.FRunning and FCtrl.FWDSPReady then
FCtrl.ServiceBeaconLock;
// Телеграф: фронты «железо в эфире» (у Pluto HP-статуса нет вовсе, и
// отпустить реле T/R после выдержки больше некому).
FCtrl.ServiceCWKeyed;
lastBeacon := nowMs;
end;
if nowMs - lastPush >= PUSH_INTERVAL_MS then
begin
// S-метр приходит из WDSP (не из сетевого HP-статуса), поэтому его никто
// не обновляет в headless — освежаем здесь, как GUI-таймер (MainForm). Без
// этого PushState слал бы клиентам залипший -130 dBm.
if FCtrl.FRunning and FCtrl.FWDSPReady then
FCtrl.FLastSMeter := FCtrl.ReadSMeterDBm;
FWebAd.PushState; // исходящее зеркало состояния/спектра
lastPush := nowMs;
end;
end;
Log('Shutting down...');
if FCtrl.FNetwork.Connected then
FCtrl.StopAndDisconnect; // персист настроек + graceful Run=0
end;
begin
TraceInit;
OnGetApplicationName := @DaemonAppName; // ДО создания контроллера/настроек!
Log('EWSDR daemon starting (headless).');
GHost := THeadlessHost.Create;
{$IFDEF UNIX}
fpSignal(SIGINT, @HandleSignal);
fpSignal(SIGTERM, @HandleSignal);
{$ENDIF}
try
GHost.Run;
finally
FreeAndNil(GHost);
end;
Log('EWSDR daemon stopped.');
end.