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; procedure SyncBeacon; procedure SyncBeaconSeed; procedure SyncRxGainMode; procedure SyncRxGain; procedure SyncRxMute; function BeaconStatusText: string; // компактный статус (как десктоп поле 7) 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); // QO-100 beacon: вкл/выкл лок и наведение по клику в спектре (frac 0..1). procedure OnBeacon(On_: Boolean); procedure OnBeaconSeed(Frac: Double); procedure OnRxGainMode(Mode: Integer); procedure OnRxGain(Db: Integer); procedure OnRxMute(On_: Boolean); 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 < MODE_MIN) or (FSyncInt > MODE_MAX) 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.OnBeacon(On_: Boolean); begin FSyncBool := On_; FController.Invoke(@SyncBeacon); end; procedure TWebAdapter.SyncBeacon; begin // Вкл/выкл лок маяка (= запуск декодера + трим LOError). Идемпотентно. FController.SetBeaconLock(FSyncBool); end; procedure TWebAdapter.OnBeaconSeed(Frac: Double); begin FSyncFreq := Frac; FController.Invoke(@SyncBeaconSeed); end; procedure TWebAdapter.SyncBeaconSeed; var Hz, VC, VS: Double; begin // Frac (доля 0..1 по ширине спектра) → абс. display-Hz от ЖИВОГО видимого окна // контроллера (зеркалит десктоп PixelToFreq с учётом зума). Так наведение // точно, даже когда LO ретюнится локом и клиентский center отстаёт. if (FSyncFreq < 0) or (FSyncFreq > 1) then Exit; FController.GetViewWindow(VC, VS); Hz := VC + (FSyncFreq - 0.5) * VS; FController.BeaconSeedAtHz(Hz); end; procedure TWebAdapter.OnRxGainMode(Mode: Integer); begin FSyncInt := Mode; FController.Invoke(@SyncRxGainMode); end; procedure TWebAdapter.SyncRxGainMode; begin // Pluto hw-AGC: 0=manual,1=fast,2=slow,3=hybrid (рендер через rfRxGain). FController.SetRxGainMode(FSyncInt); end; procedure TWebAdapter.OnRxGain(Db: Integer); begin FSyncInt := Db; FController.Invoke(@SyncRxGain); end; procedure TWebAdapter.SyncRxGain; begin // Ручной hw-gain (0..73 дБ); SetRxGain переводит режим в manual. FController.SetRxGain(FSyncInt); end; procedure TWebAdapter.OnRxMute(On_: Boolean); begin FSyncBool := On_; FController.Invoke(@SyncRxMute); end; procedure TWebAdapter.SyncRxMute; begin // QO-100 self-monitor: RX-аудио на TX (toggle, рендер через rfRxMuteOnTx). FController.SetRxMuteOnTx(FSyncBool); 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; // ── Исходящее зеркало состояния ────────────────────────────────────────────── function TWebAdapter.BeaconStatusText: string; // Зеркалит TMainForm.BeaconStatusText (поле 7 десктопа). Pluto в трансвертере. begin if not FController.BeaconLockEnabled then Exit('BCN: off'); if FController.BeaconDecodeFreqHz <= 0 then Exit('BCN: click beacon'); case FController.BeaconState of bsLock: Result := Format('BCN LOCK %.0fHz /%.0fdB', [FController.BeaconCorrectionHz, FController.BeaconSNRdB]); bsSearch: Result := Format('BCN sync /%.0fdB', [FController.BeaconSNRdB]); else Result := 'BCN --'; end; end; procedure TWebAdapter.PushState; var StatusText, BoardText, IPText, SupplyText: string; PLLText, RXText, TXText, SeqText: string; ViewCenter, ViewSpan: Double; WfNew: Boolean; begin // Пришла ли от DSP новая строка водопада с прошлого пуша. Снимаем ДО пуша: // без этого сервер повторно отправлял бы прежнюю строку, когда кадров нет // (RX остановлен) или когда пуш случился чаще кадров — водопад двоил. WfNew := FController.FWfAccumFresh; // Видимое окно (центр+span) с учётом зума: пиксели спектра уже зумлены // анализатором, web-метки/маппинг должны соответствовать. FController.GetViewWindow(ViewCenter, ViewSpan); 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 — температуры (трансивер/SoC) + RSSI (telemetry-поле). // Temp X/YC: X — трансивер AD9361, Y — Zynq SoC/FPGA (xadc). SupplyText := ''; if (FController.FLastPlutoTempC > TELEMETRY_NONE) and (FController.FLastPlutoSocTempC > TELEMETRY_NONE) then SupplyText := Format('Temp %.0f/%.0fC', [FController.FLastPlutoTempC, FController.FLastPlutoSocTempC]) else if FController.FLastPlutoTempC > TELEMETRY_NONE then SupplyText := Format('Temp %.0fC', [FController.FLastPlutoTempC]) else if FController.FLastPlutoSocTempC > TELEMETRY_NONE then SupplyText := Format('Temp %.0fC', [FController.FLastPlutoSocTempC]); 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 --'; // QO-100 beacon lock — отдельный канал состояния (видим только на Pluto в XVTR). FServer.SetBeaconStatus( FController.IsPluto and (FController.FCurrentXvtr >= 0), FController.BeaconLockEnabled, BeaconStatusText, FController.BeaconRefHz, FController.BeaconTrackedFreqHz); // Pluto RF-AGC (виден при HasHWGain = Pluto подключён) + QO-100 RX MUTE (InQO100). FServer.SetRxGainStatus( FController.BackendCaps.HasHWGain, FController.FRxGainMode, FController.FRxGainDb); FServer.SetRxMuteStatus( FController.InQO100, FController.FRxMuteOnTx); // Счётчики — РЕАЛЬНЫЕ: кадр короче потолка (web.spec_pixels), когда // разрешение анализатора (= ширина панадаптера) меньше него — узкая панель, // грид из двух колонок. Константа здесь отправляла хвост буфера — нули // (0 dBm) поверх шкалы и сжатый по частоте спектр. FServer.PushSpectrum( FController.FSpectrumBuf, FController.FSpectrumBufCount, FController.FWaterfallBuf, FController.FWaterfallBufCount, WfNew, FController.FLastSMeter, FController.FVfoA, FController.FMode, FController.FFilterBW, FController.FAGCMode, FController.FAGCTop, ViewSpan, FController.ActiveVolume, 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, ViewCenter, 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); // Строка забрана — следующий кадр DSP начинает новое накопление. if WfNew then FController.FWfAccumFresh := False; end; end.