mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
Phase 5 (D4): headless daemon entry point (ewsdrd)
ewsdrd.lpr — a console daemon that runs TRadioController + TWebAdapter WITHOUT
TMainForm/LCL, proving the Phase-5 goal: web-only headless operation. THeadlessHost
wires the data-path callbacks to the controller, the web command handlers to the
adapter (lifecycle ones marshalled to the main thread via FCtrl.Invoke =
TThread.Synchronize), opens WDSP synchronously, autostarts the saved device, and
runs a CheckSynchronize + periodic PushState loop. SIGINT/SIGTERM → graceful
StopAndDisconnect (persist + Run=0).
To keep the controller graph truly LCL-free, PlatformUtils now guards its only
Forms dependency (Screen.PixelsPerInch in CurrentScreenDPI) behind {$IFNDEF
HEADLESS}; the GUI build is byte-identical (HEADLESS undefined). build-ewsdrd.sh
compiles the headless graph from source with -Mobjfpc (nested comments) -dHEADLESS
into a separate lib-headless/ output dir, untouching the GUI .ppu.
Verified: builds clean; runs headless, web server up (HTTP 401 = serving), no LCL;
SIGINT shuts down gracefully. Radio-connect + web round-trip need hardware to test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b9c358789d
commit
de234b56e5
@@ -97,6 +97,9 @@ modules/
|
||||
backup/
|
||||
*.bak
|
||||
lib/
|
||||
lib-headless/
|
||||
ewsdr_test_checklist.html
|
||||
ewsdr-test-report-*.md
|
||||
|
||||
# Application bundle for Mac OS
|
||||
*.app/
|
||||
|
||||
+5
-1
@@ -22,7 +22,7 @@ function GetAppCfgDir: string;
|
||||
implementation
|
||||
|
||||
uses
|
||||
SysUtils, Forms;
|
||||
SysUtils {$IFNDEF HEADLESS}, Forms{$ENDIF};
|
||||
|
||||
function QtPlatformAllowsOpenGLControls: Boolean;
|
||||
var
|
||||
@@ -62,8 +62,12 @@ end;
|
||||
|
||||
function CurrentScreenDPI: Integer;
|
||||
begin
|
||||
{$IFNDEF HEADLESS}
|
||||
Result := Screen.PixelsPerInch;
|
||||
if Result <= 0 then Result := 96;
|
||||
{$ELSE}
|
||||
Result := 96; // headless-демон без LCL: экрана нет, дефолт DPI
|
||||
{$ENDIF}
|
||||
end;
|
||||
|
||||
function GetAppCfgDir: string;
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# Сборка headless-демона ewsdrd (только Web-управление, без LCL/виджетсета).
|
||||
#
|
||||
# Компилирует headless-граф ИЗ ИСХОДНИКОВ с -dHEADLESS в ОТДЕЛЬНЫЙ каталог
|
||||
# (lib-headless/...), чтобы не трогать .ppu GUI-сборки. Ключевое:
|
||||
# -Mobjfpc — командный режим по умолчанию: включает nested comments, нужные
|
||||
# для лидирующих комментариев Settings.pas (юниты дальше сами ставят
|
||||
# свой {$mode} директивой; программа — {$MODE Delphi}).
|
||||
# -dHEADLESS — PlatformUtils не тянет Forms (LCL), граф остаётся LCL-free.
|
||||
# libwdsp.dylib должна лежать в /usr/local/lib.
|
||||
set -e
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
cd "$ROOT"
|
||||
CPU=$(fpc -iTP)
|
||||
OS=$(fpc -iTO)
|
||||
OUTUNITS="lib-headless/${CPU}-${OS}"
|
||||
OUT="bin/${CPU}-${OS}"
|
||||
mkdir -p "$OUTUNITS" "$OUT"
|
||||
fpc -Mobjfpc -O3 -dHEADLESS \
|
||||
-Fu"$ROOT" -FU"$OUTUNITS" -k-L/usr/local/lib \
|
||||
-o"$OUT/ewsdrd" ewsdrd.lpr
|
||||
echo "Built: $OUT/ewsdrd"
|
||||
+402
@@ -0,0 +1,402 @@
|
||||
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;
|
||||
|
||||
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 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;
|
||||
|
||||
{$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.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;
|
||||
begin
|
||||
if FCtrl.FWDSPReady then Exit(True);
|
||||
Log('Opening WDSP...');
|
||||
Result := FCtrl.FDSPEngine.Open;
|
||||
FCtrl.FWDSPReady := Result;
|
||||
if not Result then
|
||||
Log('WDSP open FAILED: ' + FCtrl.FDSPEngine.LastError);
|
||||
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(@FCtrl.Discover); end; // Discover уже headless
|
||||
|
||||
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;
|
||||
var
|
||||
Dev: THPSDRDevice;
|
||||
i: Integer;
|
||||
begin
|
||||
if FCtrl.FNetwork.Connected then Exit;
|
||||
FillChar(Dev, SizeOf(Dev), 0);
|
||||
Dev.IPAddress := FsStr1;
|
||||
// BoardType не приходит при Connect — берём из discovery, иначе из saved по IP.
|
||||
i := FCtrl.FDeviceStore.FindDiscoveredByIP(FsStr1);
|
||||
if i >= 0 then
|
||||
Dev.BoardType := FCtrl.FDeviceStore.Discovered(i).BoardType
|
||||
else
|
||||
for i := 0 to FCtrl.FDeviceStore.SavedCount - 1 do
|
||||
if FCtrl.FDeviceStore.Saved(i).IPAddress = FsStr1 then
|
||||
begin
|
||||
Dev.BoardType := FCtrl.FDeviceStore.Saved(i).BoardType;
|
||||
Break;
|
||||
end;
|
||||
ConnectTo(Dev);
|
||||
end;
|
||||
|
||||
procedure THeadlessHost.SyncDisconnect;
|
||||
begin
|
||||
if FCtrl.FNetwork.Connected then
|
||||
begin
|
||||
FCtrl.StopAndDisconnect;
|
||||
Log('Disconnected (web).');
|
||||
end;
|
||||
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.FDeviceStore.AddSaved(FsStr1, FsStr2, 0); 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;
|
||||
AutoIP: string;
|
||||
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;
|
||||
|
||||
// Autostart — подключаемся сразу к отмеченному устройству.
|
||||
AutoIP := FCtrl.FDeviceStore.AutoStartIP;
|
||||
if AutoIP <> '' then
|
||||
begin
|
||||
FillChar(Dev, SizeOf(Dev), 0);
|
||||
Dev.IPAddress := AutoIP;
|
||||
Dev.BoardType := FCtrl.FDeviceStore.AutoStartBoardType;
|
||||
ConnectTo(Dev);
|
||||
end
|
||||
else
|
||||
Log('No autostart device — waiting for web connect.');
|
||||
|
||||
FRunning := True;
|
||||
lastPush := GetTickCount64;
|
||||
while FRunning do
|
||||
begin
|
||||
CheckSynchronize(PUSH_INTERVAL_MS); // исполняет очередь Invoke + ждёт до 50мс
|
||||
nowMs := GetTickCount64;
|
||||
if nowMs - lastPush >= PUSH_INTERVAL_MS then
|
||||
begin
|
||||
FWebAd.PushState; // исходящее зеркало состояния/спектра
|
||||
lastPush := nowMs;
|
||||
end;
|
||||
end;
|
||||
|
||||
Log('Shutting down...');
|
||||
if FCtrl.FNetwork.Connected then
|
||||
FCtrl.StopAndDisconnect; // персист настроек + graceful Run=0
|
||||
end;
|
||||
|
||||
begin
|
||||
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.
|
||||
Reference in New Issue
Block a user