mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:27:33 +00:00
The headless daemon (ewsdrd) only ever built HPSDR devices, so Pluto could not be used: backend is chosen by Dev.Kind and Pluto opens by URI, neither of which the daemon propagated. Also local audio played on the host and web Discover never probed network Plutos. Shared, backend-agnostic helpers in TRadioController (used by GUI + daemon): - ResolveDevice(IP): discovered->saved lookup restoring Kind/URI/Serial/ BoardType/MAC. MainForm.ResolveDevice now delegates here. - AddSavedDevice(name, addr): web dev_add saves Pluto when addr has a URI scheme (ip:/usb:/local:), else HPSDR. Both web hosts use it. - SeedPlutoProbeFromSaved: seed network-probe URIs from saved Plutos (no mDNS -> a network Pluto is only found by direct URI probe). - LocalAudioEnabled flag (GUI=True): when False the local sound card is neither opened nor written; RX audio goes only to web (OnAudioConsume). DeviceStore.AutoStartDevice(out Dev): full autostart record (Kind/URI/ Serial) so a saved Pluto (URI-addressed, empty IPAddress on USB) can autostart. Daemon (ewsdrd.lpr): LocalAudioEnabled:=False; SyncConnect via ResolveDevice; autostart via AutoStartDevice; SyncDiscover seeds probe URIs then Discover; SyncDevAdd via AddSavedDevice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
444 lines
18 KiB
ObjectPascal
444 lines
18 KiB
ObjectPascal
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,
|
||
RadioController, WebServer, WebAdapter,
|
||
HPSDRNetwork, Settings, WDSP, PlatformUtils, DeviceStore;
|
||
|
||
const
|
||
DAEMON_SPEC_WIDTH = 1024; // ширина спектра для StartRunning (web-буфер = 1024)
|
||
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;
|
||
procedure ConnectTo(const Dev: THPSDRDevice);
|
||
procedure PushDeviceList;
|
||
procedure PushXvtr;
|
||
// Входящие 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);
|
||
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.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; end; // connect / xvtr-смена
|
||
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 рисует лишь ~6% ширины, остальное белым. Ставим ровно под web-буфер
|
||
// (SPECTRUM_PIXELS=1024). FLastSpectrumW переживает пересоздание канала.
|
||
FCtrl.FDSPEngine.SetSpectrumWidth(DAEMON_SPEC_WIDTH);
|
||
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, DAEMON_SPEC_WIDTH) 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;
|
||
|
||
// --- Входящие 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, 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;
|
||
|
||
// 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;
|
||
while FRunning do
|
||
begin
|
||
CheckSynchronize(PUSH_INTERVAL_MS); // исполняет очередь Invoke + ждёт до 50мс
|
||
nowMs := GetTickCount64;
|
||
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.FDSPEngine.GetSMeterDBm;
|
||
FWebAd.PushState; // исходящее зеркало состояния/спектра
|
||
lastPush := nowMs;
|
||
end;
|
||
end;
|
||
|
||
Log('Shutting down...');
|
||
if FCtrl.FNetwork.Connected then
|
||
FCtrl.StopAndDisconnect; // персист настроек + graceful Run=0
|
||
end;
|
||
|
||
begin
|
||
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.
|