mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
- Status bar / web now show Pluto/LibreSDR model + serial instead of "Unknown Board" (BoardDisplayName in controller; UI just renders it). - Field 3 shows AD936x chip temperature + RX RSSI for Pluto (no PA → Supply only for openHPSDR). Polled via backend.ReadTelemetry (~2 Hz), filtered for sensor glitches (plausibility window + slew-limit, with anti-stick) — fixes rare bogus -15°C readings. - Add libiio binding iio_channel_attr_read_double. - Remove stale root pluto_integration.MD (superseded by doc/PLUTO_INTEGRATION_PLAN.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
478 lines
18 KiB
ObjectPascal
478 lines
18 KiB
ObjectPascal
unit WebAdapter;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
// Адаптер веб-интерфейса: мост между TWebServer (входящие команды из WS-потока)
|
|
// и TRadioController (ядро). Раньше эти WebOnXxx/SyncWebXxx жили в TMainForm —
|
|
// вынесены сюда, чтобы развязать UI от веба и подготовить headless-демон.
|
|
//
|
|
// Маршалинг: входящие команды приходят из потока веб-сервера; выполняем их в
|
|
// потоке контроллера через FController.Invoke (GUI = TThread.Synchronize; демон
|
|
// подставит свою очередь). Synchronize блокирующий и последовательный — поля
|
|
// FSync* для передачи параметров безопасны (гонок нет).
|
|
//
|
|
// Часть команд (тюнинг VFO/диапазон/MOX/...) идёт через UI-обёртки MainForm,
|
|
// которые ещё не headless — для них есть IWebHost (его реализует хозяин). По мере
|
|
// переноса оркестрации в контроллер интерфейс будет сужаться.
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils,
|
|
RadioController, WebServer, WDSPEngine, HPSDRNetwork, HPSDRProtocol, RadioBackend, BoardUtils, Settings;
|
|
|
|
type
|
|
// UI/оркестрационные операции, которые адаптер дёргает у хозяина (пока не
|
|
// headless — десктоп: методы MainForm; демон: своя реализация). Наполняется
|
|
// в WA2 по мере переноса host-зависимых хендлеров.
|
|
IWebHost = interface
|
|
['{B3A1F7C2-9D54-4E18-9C6B-2E5A7F0D1A33}']
|
|
end;
|
|
|
|
{ TWebAdapter }
|
|
TWebAdapter = class
|
|
private
|
|
FController: TRadioController;
|
|
FServer: TWebServer;
|
|
FHost: IWebHost;
|
|
// Поля для передачи параметров команды в Sync-метод (через Invoke).
|
|
FSyncInt: Integer;
|
|
FSyncBool: Boolean;
|
|
FSyncFreq: Double;
|
|
// Sync-методы (выполняются в потоке контроллера через Invoke).
|
|
procedure SyncMode;
|
|
procedure SyncFilter;
|
|
procedure SyncAGC;
|
|
procedure SyncAGCTop;
|
|
procedure SyncVolume;
|
|
procedure SyncDrive;
|
|
procedure SyncSpan;
|
|
procedure SyncBand;
|
|
procedure SyncXvtrBand;
|
|
procedure SyncWfAGC;
|
|
procedure SyncWfNF;
|
|
procedure SyncMute;
|
|
procedure SyncNR;
|
|
procedure SyncNB;
|
|
procedure SyncSNB;
|
|
procedure SyncANF;
|
|
procedure SyncFreq;
|
|
procedure SyncFreqA;
|
|
procedure SyncFreqB;
|
|
procedure SyncCenter;
|
|
procedure SyncAttn;
|
|
procedure SyncActiveVfo;
|
|
procedure SyncCtun;
|
|
procedure SyncMOX;
|
|
procedure SyncTun;
|
|
procedure SyncFMStep;
|
|
public
|
|
constructor Create(AController: TRadioController; AServer: TWebServer;
|
|
AHost: IWebHost);
|
|
|
|
// Колбэки веб-сервера (навешиваются хозяином на FServer.OnXxx).
|
|
procedure OnMode(Mode: Integer);
|
|
procedure OnFilter(BW: Integer);
|
|
procedure OnAGC(Mode: Integer);
|
|
procedure OnAGCTop(DB: Integer);
|
|
procedure OnVolume(V: Integer);
|
|
procedure OnDrive(V: Integer);
|
|
procedure OnSpan(Hz: Integer);
|
|
procedure OnBand(Idx: Integer);
|
|
procedure OnXvtrBand(Idx: Integer);
|
|
procedure OnWfAGC(On_: Boolean);
|
|
procedure OnWfNF(On_: Boolean);
|
|
procedure OnMute(On_: Boolean);
|
|
procedure OnNR(Mode: Integer);
|
|
procedure OnNB(Mode: Integer);
|
|
procedure OnSNB(On_: Boolean);
|
|
procedure OnANF(On_: Boolean);
|
|
procedure OnFreq(Hz: Double);
|
|
procedure OnFreqA(Hz: Double);
|
|
procedure OnFreqB(Hz: Double);
|
|
procedure OnCenter(Hz: Double);
|
|
procedure OnAttn(Idx: Integer);
|
|
procedure OnActiveVfo(Idx: Integer);
|
|
procedure OnCtun(On_: Boolean);
|
|
procedure OnMOX(On_: Boolean);
|
|
procedure OnTun(On_: Boolean);
|
|
procedure OnFMStep(Idx: Integer);
|
|
procedure OnClientActiveChanged(Active: Boolean);
|
|
// TX-mic от web-клиента → WDSP. Вызывается из WS-потока; без маршалинга —
|
|
// подача в движок thread-safe (как OnMicPacket/OnDDCIQ контроллера).
|
|
procedure OnMic(Samples: PSingle; Count: Integer);
|
|
|
|
// RX-аудио-перехват: навешивается на FController.OnAudioConsume. Если есть
|
|
// активный web-клиент — сводим в моно и шлём ему, возвращаем True (локальный
|
|
// AudioOut пропускается контроллером). Вызывается из DSP-потока.
|
|
function AudioConsume(const Left, Right: array of Single;
|
|
Count: Integer): Boolean;
|
|
|
|
// Исходящее зеркало: периодический снимок состояния + спектр/водопад в web.
|
|
// Зовётся хозяином из GUI-таймера; всё (включая последний кадр спектра/
|
|
// водопада) читается из FController. Сервер сам гейтит по FRunning и шлёт
|
|
// активным клиентам.
|
|
procedure PushState;
|
|
end;
|
|
|
|
implementation
|
|
|
|
constructor TWebAdapter.Create(AController: TRadioController;
|
|
AServer: TWebServer; AHost: IWebHost);
|
|
begin
|
|
inherited Create;
|
|
FController := AController;
|
|
FServer := AServer;
|
|
FHost := AHost;
|
|
end;
|
|
|
|
// ── Входящие команды: запоминаем параметр, выполняем в потоке контроллера ─────
|
|
|
|
procedure TWebAdapter.OnMode(Mode: Integer);
|
|
begin FSyncInt := Mode; FController.Invoke(@SyncMode); end;
|
|
|
|
procedure TWebAdapter.SyncMode;
|
|
begin
|
|
if (FSyncInt < 0) or (FSyncInt > MODE_SAM) then Exit;
|
|
FController.SetMode(FSyncInt); // дефолт фильтра + DSP + cache; рендер через rfMode
|
|
end;
|
|
|
|
procedure TWebAdapter.OnFilter(BW: Integer);
|
|
begin FSyncInt := BW; FController.Invoke(@SyncFilter); end;
|
|
|
|
procedure TWebAdapter.SyncFilter;
|
|
begin
|
|
// Web-клиент шлёт полосу в Гц (кнопки панели фильтров = пресетные значения);
|
|
// команда применяет её и подсвечивает ближайший пресет (рендер — rfFilter).
|
|
FController.SetFilterBW(FSyncInt);
|
|
end;
|
|
|
|
procedure TWebAdapter.OnAGC(Mode: Integer);
|
|
begin FSyncInt := Mode; FController.Invoke(@SyncAGC); end;
|
|
|
|
procedure TWebAdapter.SyncAGC;
|
|
begin
|
|
FController.SetAGCMode(FSyncInt); // единый маппинг (исправляет старый web-баг)
|
|
end;
|
|
|
|
procedure TWebAdapter.OnAGCTop(DB: Integer);
|
|
begin FSyncInt := DB; FController.Invoke(@SyncAGCTop); end;
|
|
|
|
procedure TWebAdapter.SyncAGCTop;
|
|
begin
|
|
FController.SetAGCTop(FSyncInt); // клампит, применяет, OnControllerState обновит UI
|
|
end;
|
|
|
|
procedure TWebAdapter.OnVolume(V: Integer);
|
|
begin FSyncInt := V; FController.Invoke(@SyncVolume); end;
|
|
|
|
procedure TWebAdapter.SyncVolume;
|
|
begin FController.SetVolume(FSyncInt); end;
|
|
|
|
procedure TWebAdapter.OnDrive(V: Integer);
|
|
begin FSyncInt := V; FController.Invoke(@SyncDrive); end;
|
|
|
|
procedure TWebAdapter.SyncDrive;
|
|
begin FController.SetDrive(FSyncInt); end; // rfDrive снимет память канала (ChannelController)
|
|
|
|
procedure TWebAdapter.OnBand(Idx: Integer);
|
|
begin FSyncInt := Idx; FController.Invoke(@SyncBand); end;
|
|
|
|
procedure TWebAdapter.SyncBand;
|
|
begin FController.SetBand(FSyncInt); end; // XVTR-выход + persist + restore — в команде
|
|
|
|
procedure TWebAdapter.OnXvtrBand(Idx: Integer);
|
|
begin FSyncInt := Idx; FController.Invoke(@SyncXvtrBand); end;
|
|
|
|
procedure TWebAdapter.SyncXvtrBand;
|
|
begin
|
|
if FSyncInt < 0 then begin FController.SetXvtrBand(-1); Exit; end;
|
|
if FSyncInt >= CFG_XVTR_COUNT then Exit;
|
|
if not FController.FXvtrSettings.Entries[FSyncInt].Enabled then Exit;
|
|
if FSyncInt = FController.FCurrentXvtr then Exit;
|
|
if FController.FDevConnected then
|
|
begin FController.SaveCurrentBand; FController.FSettings.Save; end;
|
|
FController.SetXvtrBand(FSyncInt); // ActivateXvtr (рендер/wideband/web — rfXvtr)
|
|
end;
|
|
|
|
procedure TWebAdapter.OnSpan(Hz: Integer);
|
|
begin FSyncInt := Hz; FController.Invoke(@SyncSpan); end;
|
|
|
|
procedure TWebAdapter.SyncSpan;
|
|
// span ≡ sample rate. Принимаем только валидные значения (как десктоп-оверлей);
|
|
// SetSampleRate сам пересоздаёт WDSP-канал + персистит (idempotent при том же rate).
|
|
const
|
|
ValidSpans: array[0..5] of Integer = (48000, 96000, 192000, 384000, 768000, 1536000);
|
|
var i: Integer;
|
|
begin
|
|
for i := 0 to High(ValidSpans) do
|
|
if ValidSpans[i] = FSyncInt then
|
|
begin
|
|
FController.SetSampleRate(FSyncInt);
|
|
Exit;
|
|
end;
|
|
end;
|
|
|
|
procedure TWebAdapter.OnWfAGC(On_: Boolean);
|
|
begin FSyncBool := On_; FController.Invoke(@SyncWfAGC); end;
|
|
|
|
procedure TWebAdapter.SyncWfAGC;
|
|
begin FController.SetWfAGC(FSyncBool); end;
|
|
|
|
procedure TWebAdapter.OnWfNF(On_: Boolean);
|
|
begin FSyncBool := On_; FController.Invoke(@SyncWfNF); end;
|
|
|
|
procedure TWebAdapter.SyncWfNF;
|
|
begin FController.SetWfNF(FSyncBool); end;
|
|
|
|
procedure TWebAdapter.OnMute(On_: Boolean);
|
|
begin FSyncBool := On_; FController.Invoke(@SyncMute); end;
|
|
|
|
procedure TWebAdapter.SyncMute;
|
|
begin FController.SetMute(FSyncBool); end; // идемпотентно; OnControllerState обновит UI
|
|
|
|
procedure TWebAdapter.OnNR(Mode: Integer);
|
|
begin FSyncInt := Mode; FController.Invoke(@SyncNR); end;
|
|
|
|
procedure TWebAdapter.SyncNR;
|
|
begin FController.SetNR(FSyncInt); end; // клампит и применяет; рендер через rfNR
|
|
|
|
procedure TWebAdapter.OnNB(Mode: Integer);
|
|
begin FSyncInt := Mode; FController.Invoke(@SyncNB); end;
|
|
|
|
procedure TWebAdapter.SyncNB;
|
|
begin FController.SetNB(FSyncInt); end;
|
|
|
|
procedure TWebAdapter.OnSNB(On_: Boolean);
|
|
begin FSyncBool := On_; FController.Invoke(@SyncSNB); end;
|
|
|
|
procedure TWebAdapter.SyncSNB;
|
|
begin FController.SetSNB(FSyncBool); end;
|
|
|
|
procedure TWebAdapter.OnANF(On_: Boolean);
|
|
begin FSyncBool := On_; FController.Invoke(@SyncANF); end;
|
|
|
|
procedure TWebAdapter.SyncANF;
|
|
begin FController.SetANF(FSyncBool); end;
|
|
|
|
procedure TWebAdapter.OnFreq(Hz: Double);
|
|
begin FSyncFreq := Hz; FController.Invoke(@SyncFreq); end;
|
|
|
|
procedure TWebAdapter.SyncFreq;
|
|
begin
|
|
// "freq" для АКТИВНОГО VFO. SetVfoA/SetVfoB несут CTUN/band-detect/сеть +
|
|
// канальную оркестрацию (через OnAfterTune-хук); рендер — rfVfoA/rfVfoB.
|
|
if FController.FActiveVfo = 0 then FController.SetVfoA(FSyncFreq)
|
|
else FController.SetVfoB(FSyncFreq);
|
|
end;
|
|
|
|
procedure TWebAdapter.OnFreqA(Hz: Double);
|
|
begin FSyncFreq := Hz; FController.Invoke(@SyncFreqA); end;
|
|
|
|
procedure TWebAdapter.SyncFreqA;
|
|
begin FController.SetVfoA(FSyncFreq); end;
|
|
|
|
procedure TWebAdapter.OnFreqB(Hz: Double);
|
|
begin FSyncFreq := Hz; FController.Invoke(@SyncFreqB); end;
|
|
|
|
procedure TWebAdapter.SyncFreqB;
|
|
begin
|
|
// Унифицировано с десктопом: команда хранит частоту, при активном B
|
|
// перестраивает приёмник (CTUN-aware) + сеть; рендер — через rfVfoB/rfBand.
|
|
FController.SetVfoB(FSyncFreq);
|
|
end;
|
|
|
|
procedure TWebAdapter.OnCenter(Hz: Double);
|
|
begin FSyncFreq := Hz; FController.Invoke(@SyncCenter); end;
|
|
|
|
procedure TWebAdapter.SyncCenter;
|
|
begin
|
|
// Сдвигаем DDC-центр без смены VFO (аналог CTUN-drag в десктопе).
|
|
FController.SetCenter(FSyncFreq);
|
|
end;
|
|
|
|
procedure TWebAdapter.OnAttn(Idx: Integer);
|
|
begin FSyncInt := Idx; FController.Invoke(@SyncAttn); end;
|
|
|
|
procedure TWebAdapter.SyncAttn;
|
|
begin FController.SetAtten(FSyncInt); end;
|
|
|
|
procedure TWebAdapter.OnActiveVfo(Idx: Integer);
|
|
begin FSyncInt := Idx; FController.Invoke(@SyncActiveVfo); end;
|
|
|
|
procedure TWebAdapter.SyncActiveVfo;
|
|
begin
|
|
if (FSyncInt < 0) or (FSyncInt > 1) then Exit;
|
|
if FSyncInt = FController.FActiveVfo then Exit;
|
|
FController.SetActiveVfo(FSyncInt); // перестройка RX-VFO; рендер rfActiveVfo
|
|
end;
|
|
|
|
procedure TWebAdapter.OnCtun(On_: Boolean);
|
|
begin FSyncBool := On_; FController.Invoke(@SyncCtun); end;
|
|
|
|
procedure TWebAdapter.SyncCtun;
|
|
begin
|
|
if FSyncBool <> FController.FCTun then
|
|
FController.SetCTun(FSyncBool); // band cache + center/shift + сеть; рендер rfCTun
|
|
end;
|
|
|
|
procedure TWebAdapter.OnMOX(On_: Boolean);
|
|
begin FSyncBool := On_; FController.Invoke(@SyncMOX); end;
|
|
|
|
procedure TWebAdapter.SyncMOX;
|
|
begin
|
|
// FWebClientActive держится актуальным OnClientActiveChanged — выбор mic-source.
|
|
FController.SetMOX(FSyncBool); // ядро TX (safety/PTT/DSP); рендер rfTransmitting
|
|
end;
|
|
|
|
procedure TWebAdapter.OnTun(On_: Boolean);
|
|
begin FSyncBool := On_; FController.Invoke(@SyncTun); end;
|
|
|
|
procedure TWebAdapter.SyncTun;
|
|
begin
|
|
FController.SetTune(FSyncBool); // ядро TUN (safety/тон/drive/MOX); рендер rfTuning
|
|
end;
|
|
|
|
procedure TWebAdapter.OnFMStep(Idx: Integer);
|
|
begin FSyncInt := Idx; FController.Invoke(@SyncFMStep); end;
|
|
|
|
procedure TWebAdapter.SyncFMStep;
|
|
begin
|
|
FController.SetFMStep(FSyncInt); // клампит 0..3 внутри; рендер rfFMStepIdx
|
|
FController.SaveCurrentBand;
|
|
end;
|
|
|
|
procedure TWebAdapter.OnClientActiveChanged(Active: Boolean);
|
|
// Из потока веб-сервера при connect/disconnect клиента. Простая запись флага —
|
|
// ядро (SetMOX, в т.ч. HWPTT-путь) выбирает по нему mic-source. Без маршалинга.
|
|
begin
|
|
FController.FWebClientActive := Active;
|
|
end;
|
|
|
|
procedure TWebAdapter.OnMic(Samples: PSingle; Count: Integer);
|
|
var
|
|
Buf: array[0..5759] of Double;
|
|
SP: PSingle;
|
|
i, n: Integer;
|
|
begin
|
|
if not FController.FWDSPReady then Exit;
|
|
n := Count;
|
|
if n > Length(Buf) then n := Length(Buf);
|
|
SP := Samples;
|
|
for i := 0 to n - 1 do
|
|
begin
|
|
Buf[i] := SP^;
|
|
Inc(SP);
|
|
end;
|
|
FController.FDSPEngine.PushTXMicSamplesD(Buf, n);
|
|
end;
|
|
|
|
function TWebAdapter.AudioConsume(const Left, Right: array of Single;
|
|
Count: Integer): Boolean;
|
|
var
|
|
MonoBuf: array[0..1023] of Single;
|
|
i, n: Integer;
|
|
begin
|
|
Result := False;
|
|
if not (Assigned(FServer) and FServer.WebClientActive) then Exit;
|
|
n := Count;
|
|
if n > Length(MonoBuf) then n := Length(MonoBuf);
|
|
for i := 0 to n - 1 do
|
|
MonoBuf[i] := (Left[i] + Right[i]) * 0.5;
|
|
FServer.PushAudio(@MonoBuf[0], n);
|
|
Result := True;
|
|
end;
|
|
|
|
// ── Исходящее зеркало состояния ──────────────────────────────────────────────
|
|
|
|
procedure TWebAdapter.PushState;
|
|
var
|
|
StatusText, BoardText, IPText, SupplyText: string;
|
|
PLLText, RXText, TXText, SeqText: string;
|
|
begin
|
|
if FController.FNetwork.Connected then
|
|
begin
|
|
if FController.FRunning then StatusText := 'Running'
|
|
else StatusText := 'Connected';
|
|
BoardText := 'Board: ' + FController.BoardDisplayName;
|
|
IPText := 'IP: ' + FController.FNetwork.Device.IPAddress;
|
|
end
|
|
else
|
|
begin
|
|
StatusText := 'Disconnected';
|
|
BoardText := 'Board --';
|
|
IPText := 'IP --';
|
|
end;
|
|
|
|
if not FController.FRunning then
|
|
begin
|
|
SupplyText := 'Supply --';
|
|
PLLText := 'PLL --';
|
|
end
|
|
else
|
|
begin
|
|
if FController.FLastPLLLock then PLLText := 'PLL OK'
|
|
else PLLText := 'PLL?';
|
|
|
|
if FController.IsPluto then
|
|
begin
|
|
// Pluto: нет PA/supply — температура чипа + RSSI приёмника (telemetry-поле).
|
|
SupplyText := '';
|
|
if FController.FLastPlutoTempC > TELEMETRY_NONE then
|
|
SupplyText := Format('Temp %.0fC', [FController.FLastPlutoTempC]);
|
|
if FController.FLastPlutoRSSIdB > TELEMETRY_NONE then
|
|
begin
|
|
if SupplyText <> '' then SupplyText := SupplyText + ' ';
|
|
SupplyText := SupplyText + Format('RSSI %.0fdB', [FController.FLastPlutoRSSIdB]);
|
|
end;
|
|
if SupplyText = '' then SupplyText := 'Pluto --';
|
|
end
|
|
else if not BoardSupportsSupplyVoltage(FController.FNetwork.Device.BoardType) then
|
|
SupplyText := 'Supply n/a'
|
|
else if FController.FLastSupplyV >= 0 then
|
|
begin
|
|
if FController.FLastSupplyA >= 0 then
|
|
SupplyText := Format('Supply %.1fV %.1fA', [FController.FLastSupplyV, FController.FLastSupplyA])
|
|
else
|
|
SupplyText := Format('Supply %.1fV', [FController.FLastSupplyV]);
|
|
end
|
|
else
|
|
SupplyText := 'Supply --';
|
|
end;
|
|
|
|
if FController.FRunning then RXText := 'RX running'
|
|
else RXText := 'RX idle';
|
|
|
|
if FController.FTuning then TXText := 'TX tune'
|
|
else if FController.FTransmitting then TXText := 'TX active'
|
|
else TXText := 'TX idle';
|
|
|
|
if FController.FSeqErrorCount > 0 then
|
|
SeqText := Format('SEQ ERR DDC%d', [FController.FLastSeqErrorDDC])
|
|
else if FController.FRunning then
|
|
SeqText := 'SEQ OK'
|
|
else
|
|
SeqText := 'SEQ --';
|
|
|
|
FServer.PushSpectrum(
|
|
FController.FSpectrumBuf, 1024,
|
|
FController.FWaterfallBuf,
|
|
FController.FLastSMeter,
|
|
FController.FVfoA, FController.FMode, FController.FFilterBW, FController.FAGCMode, FController.FAGCTop,
|
|
FController.FSpanHz, FController.FVolume,
|
|
FController.FWfAGCEnabled, FController.FWfNFEnabled,
|
|
FController.FCurrentBand,
|
|
FController.FRunning and FController.FNetwork.Connected,
|
|
FController.FRunning, FController.FMuted, FController.FCTun,
|
|
FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF,
|
|
FController.FCenterFreq, FController.FFilter,
|
|
FController.FVfoB, FController.FActiveVfo,
|
|
FController.FTransmitting, FController.FDrivePercent,
|
|
FController.FAtten, FController.FTuning, FController.FDisplayDuplex,
|
|
FController.FLastFwdW, FController.FLastSWR, FController.FPAMaxPower,
|
|
StatusText, BoardText, IPText, SupplyText,
|
|
PLLText, RXText, TXText, SeqText);
|
|
end;
|
|
|
|
end.
|