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. Валидируем по пресетам текущего бэкенда (единый источник, // как десктоп-оверлей) — иначе Pluto-рейты (960k/2304k/…) отбрасывались бы. // SetSampleRate сам пересоздаёт WDSP-канал + персистит (idempotent при том же rate). var Presets: TBackendRateArray; i: Integer; begin Presets := FController.SampleRatePresets; for i := 0 to High(Presets) do if Presets[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.