mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
Прокинул в web-интерфейс управление, которого там не хватало (раньше только отображение): - RF-AGC (Pluto hw-gain): дропдаун режима FAST/SLOW/HYB/MAN + слайдер GAIN 0–73 дБ (активен только в MAN, драг переводит в manual). Виден по BackendCaps.HasHWGain (= Pluto подключён). Команды set_rx_gain_mode / set_rx_gain → SetRxGainMode / SetRxGain. - RX MUTE (QO-100 self-monitor): toggle-кнопка рядом с BCN, видна по InQO100, подсветка по состоянию. Команда set_rx_mute → SetRxMuteOnTx. Гейтинг зеркалит десктоп. Работает в GUI-web-сервере и headless-демоне. Проверено на железе пользователем. WebServer: команды + state-поля (rf_agc_visible/rx_gain_mode/rx_gain_db, rx_mute_visible/rx_mute_on) + SetRxGainStatus/SetRxMuteStatus. WebAdapter: колбэки OnRxGain*/OnRxMute (Invoke на поток контроллера) + публикация в PushState. MainForm + ewsdrd.lpr: привязка колбэков. WebPageHtml: виджеты, JS-обработчики, updRfAgc/updRxMute, PEND_MAP. Также актуализирован doc/PLUTO_INTEGRATION_PLAN.md: TX-тракт и режим QO-100 помечены проверенными на железе; раскрыт TODO «XO ppm-калибровка» (ручной TxLOOffset как текущий обходной путь, xo_correction/GPSDO как постоянное решение). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
495 lines
20 KiB
ObjectPascal
495 lines
20 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, BoardUtils;
|
||
|
||
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;
|
||
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);
|
||
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 рисует лишь ~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;
|
||
|
||
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;
|
||
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.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.
|