diff --git a/MainForm.pas b/MainForm.pas index c5016b5..525c042 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -879,6 +879,9 @@ begin FWebServer.OnXvtrBand := FWebAdapter.OnXvtrBand; FWebServer.OnBeacon := FWebAdapter.OnBeacon; FWebServer.OnBeaconSeed := FWebAdapter.OnBeaconSeed; + FWebServer.OnRxGainMode := FWebAdapter.OnRxGainMode; + FWebServer.OnRxGain := FWebAdapter.OnRxGain; + FWebServer.OnRxMute := FWebAdapter.OnRxMute; FWebServer.OnConnect := WebOnConnect; FWebServer.OnDiscoverDev := WebOnDiscover; FWebServer.OnDisconnectDev := WebOnDisconnect; diff --git a/WebAdapter.pas b/WebAdapter.pas index 88e32eb..0d70f46 100644 --- a/WebAdapter.pas +++ b/WebAdapter.pas @@ -68,6 +68,9 @@ type procedure SyncFMStep; procedure SyncBeacon; procedure SyncBeaconSeed; + procedure SyncRxGainMode; + procedure SyncRxGain; + procedure SyncRxMute; function BeaconStatusText: string; // компактный статус (как десктоп поле 7) public constructor Create(AController: TRadioController; AServer: TWebServer; @@ -103,6 +106,9 @@ type // 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 контроллера). @@ -373,6 +379,33 @@ begin 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. Без маршалинга. @@ -512,6 +545,14 @@ begin 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); + FServer.PushSpectrum( FController.FSpectrumBuf, 1024, FController.FWaterfallBuf, diff --git a/WebPageHtml.pas b/WebPageHtml.pas index 7a67356..fdd1746 100644 --- a/WebPageHtml.pas +++ b/WebPageHtml.pas @@ -204,6 +204,13 @@ begin '' + '' + '' + + '' + + // Pluto RF-AGC (hw gain): режим FAST/SLOW/HYB/MAN + ручной gain (активен в MAN). Виден только на Pluto. + '' + + '' + '|' + '' + '' + @@ -290,6 +297,8 @@ begin 'function updSnbBtn(){sw("snbBtn",!!cur.snb);}' + 'function updAnfBtn(){sw("anfBtn",!!cur.anf);}' + 'function updBcnBtn(){const b=q("bcnBtn");if(!b)return;b.style.display=cur.beacon_visible?"":"none";sw("bcnBtn",!!cur.beacon_on);}' + + 'function updRxMute(){const b=q("rxMuteBtn");if(!b)return;b.style.display=cur.rx_mute_visible?"":"none";sw("rxMuteBtn",!!cur.rx_mute_on);}' + + 'function updRfAgc(){const vis=!!cur.rf_agc_visible;const s=q("rfagcSel"),g=q("rfgainG");if(s)s.style.display=vis?"":"none";if(g)g.style.display=vis?"":"none";if(!vis)return;const m=cur.rx_gain_mode|0;if(s&&!isPending("rxgain"))s.value=String(m);const sl=q("rfgainSl"),l=q("rfgainVal");if(sl&&!sl.matches(":active")){sl.value=cur.rx_gain_db|0;sl.disabled=(m!==0);}if(l)l.textContent=(cur.rx_gain_db|0)+"dB";}' + 'const MODE_N=["LSB","USB","DSB","CWL","CWU","FM","AM","SAM"];' + @@ -447,6 +456,9 @@ begin 'q("nrBtn").onclick=function(){cur.nr_mode=((cur.nr_mode|0)+1)%5;cur.nr=cur.nr_mode!==0;updNrBtn();pend("nr");ws2({cmd:"set_nr_mode",mode:cur.nr_mode});};' + 'q("nbBtn").onclick=function(){cur.nb_mode=((cur.nb_mode|0)+1)%3;cur.nb=cur.nb_mode!==0;updNbBtn();pend("nb");ws2({cmd:"set_nb_mode",mode:cur.nb_mode});};' + 'q("bcnBtn").onclick=function(){cur.beacon_on=!cur.beacon_on;beaconArm=cur.beacon_on;updBcnBtn();pend("beacon");cmd("set_beacon",cur.beacon_on);};' + + 'q("rxMuteBtn").onclick=function(){cur.rx_mute_on=!cur.rx_mute_on;updRxMute();pend("rxmute");cmd("set_rx_mute",cur.rx_mute_on);};' + + 'q("rfagcSel").onchange=function(e){const m=+e.target.value;cur.rx_gain_mode=m;pend("rxgain");updRfAgc();ws2({cmd:"set_rx_gain_mode",mode:m});};' + + 'q("rfgainSl").oninput=function(e){const v=+e.target.value;cur.rx_gain_db=v;cur.rx_gain_mode=0;const l=q("rfgainVal");if(l)l.textContent=v+"dB";const s=q("rfagcSel");if(s)s.value="0";pend("rxgain");ws2({cmd:"set_rx_gain",db:v});};' + 'q("snbBtn").onclick=function(){cur.snb=!cur.snb;updSnbBtn();pend("snb");cmd("set_snb",cur.snb);};' + 'q("anfBtn").onclick=function(){cur.anf=!cur.anf;updAnfBtn();pend("anf");cmd("set_anf",cur.anf);};' + @@ -681,7 +693,7 @@ begin 'function isPending(field){return !!(pendingCmds[field]&&pendingCmds[field]>Date.now());}' + // Маппинг: server JSON field → pend key - 'var PEND_MAP={vfo_a_hz:"freq",vfo_b_hz:"freq",nr:"nr",nr_mode:"nr",nb:"nb",nb_mode:"nb",snb:"snb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc",transmitting:"mox",drive:"drive",attn_idx:"attn",tuning:"tun",filter_bw:"filter",mute:"mute",beacon_on:"beacon"};' + + 'var PEND_MAP={vfo_a_hz:"freq",vfo_b_hz:"freq",nr:"nr",nr_mode:"nr",nb:"nb",nb_mode:"nb",snb:"snb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc",transmitting:"mox",drive:"drive",attn_idx:"attn",tuning:"tun",filter_bw:"filter",mute:"mute",beacon_on:"beacon",rx_mute_on:"rxmute",rx_gain_mode:"rxgain",rx_gain_db:"rxgain"};' + 'function applyState(msg){' + 'Object.keys(msg).forEach(function(k){var pk=PEND_MAP[k];if(!pk||!isPending(pk))cur[k]=msg[k];});' + @@ -728,6 +740,7 @@ begin 'if(!isPending("nr"))updNrBtn();if(!isPending("nb"))updNbBtn();' + 'if(!isPending("snb"))updSnbBtn();if(!isPending("anf"))updAnfBtn();' + 'if(!isPending("beacon"))updBcnBtn();' + + 'if(!isPending("rxmute"))updRxMute();updRfAgc();' + 'if(!isPending("run"))updRunBtn(!!cur.running);' + 'if(!isPending("mox"))updMoxBtn(!!cur.transmitting);' + 'if(!isPending("tun"))updToneBtn(!!cur.tuning);' + diff --git a/WebServer.pas b/WebServer.pas index 366b5f9..fc97e3c 100644 --- a/WebServer.pas +++ b/WebServer.pas @@ -154,6 +154,10 @@ type // QO-100 beacon lock: вкл/выкл лок и наведение (клик по маяку, доля 0..1 по ширине). TWebCmdBeacon = procedure(On_: Boolean) of object; TWebCmdBeaconSeed = procedure(Frac: Double) of object; + // Pluto RF-AGC (hw gain): режим FAST/SLOW/HYB/MAN + ручной gain дБ. RX MUTE — QO-100 self-monitor. + TWebCmdRxGainMode = procedure(Mode: Integer) of object; + TWebCmdRxGain = procedure(Db: Integer) of object; + TWebCmdRxMute = procedure(On_: Boolean) of object; // Активность web-клиента изменилась (подключился/отключился последний клиент). // Хозяин зеркалит это в ядро (FController.FWebClientActive) для выбора mic-source. TWebClientActiveEvent = procedure(Active: Boolean) of object; @@ -330,6 +334,18 @@ type FOnBeacon: TWebCmdBeacon; FOnBeaconSeed: TWebCmdBeaconSeed; + // Pluto RF-AGC (hw gain) + QO-100 RX MUTE. Зеркалится в state как + // rf_agc_visible/rx_gain_mode/rx_gain_db и rx_mute_visible/rx_mute_on; + // команды set_rx_gain_mode/set_rx_gain/set_rx_mute. + FRfAgcVisible: Boolean; // RF-AGC доступен (Pluto подключён, HasHWGain) + FRxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid + FRxGainDb: Integer; // ручной gain дБ (0..73) + FRxMuteVisible: Boolean; // RX MUTE доступна (Pluto + QO-100) + FRxMuteOn: Boolean; // self-monitor mute активен + FOnRxGainMode: TWebCmdRxGainMode; + FOnRxGain: TWebCmdRxGain; + FOnRxMute: TWebCmdRxMute; + FWebClientActive: Boolean; FOnClientActiveChanged: TWebClientActiveEvent; @@ -416,8 +432,14 @@ type // QO-100 beacon lock: зеркало статуса в state (зовётся из PushState хозяина). procedure SetBeaconStatus(AVisible, AOn: Boolean; const AText: string; ARefHz, ATrackHz: Double); + // Pluto RF-AGC + QO-100 RX MUTE: зеркало в state (зовётся из PushState хозяина). + procedure SetRxGainStatus(AVisible: Boolean; AMode, ADb: Integer); + procedure SetRxMuteStatus(AVisible, AOn: Boolean); property OnBeacon: TWebCmdBeacon read FOnBeacon write FOnBeacon; property OnBeaconSeed: TWebCmdBeaconSeed read FOnBeaconSeed write FOnBeaconSeed; + property OnRxGainMode: TWebCmdRxGainMode read FOnRxGainMode write FOnRxGainMode; + property OnRxGain: TWebCmdRxGain read FOnRxGain write FOnRxGain; + property OnRxMute: TWebCmdRxMute read FOnRxMute write FOnRxMute; property OnSpan: TWebCmdSpan read FOnSpan write FOnSpan; property OnVolume: TWebCmdVolume read FOnVolume write FOnVolume; property OnWfAGC: TWebCmdWfAGC read FOnWfAGC write FOnWfAGC; @@ -565,6 +587,11 @@ begin FBeaconText := ''; FBeaconRefHz := 0; FBeaconTrackHz := 0; + FRfAgcVisible := False; + FRxGainMode := 2; // slow_attack по умолчанию (как контроллер) + FRxGainDb := 40; + FRxMuteVisible := False; + FRxMuteOn := False; SetLength(FXvtrBands, 0); // Bootstrap-набор рейтов (до connect): хост перезапишет под бэкенд (HPSDR/Pluto). SetRatePresets([48000, 96000, 192000, 384000, 768000, 1536000]); @@ -604,6 +631,29 @@ begin end; end; +procedure TWebServer.SetRxGainStatus(AVisible: Boolean; AMode, ADb: Integer); +begin + FStateLock.Enter; + try + FRfAgcVisible := AVisible; + FRxGainMode := AMode; + FRxGainDb := ADb; + finally + FStateLock.Leave; + end; +end; + +procedure TWebServer.SetRxMuteStatus(AVisible, AOn: Boolean); +begin + FStateLock.Enter; + try + FRxMuteVisible := AVisible; + FRxMuteOn := AOn; + finally + FStateLock.Leave; + end; +end; + procedure TWebServer.SetDeviceList(const ADevices: TWebDeviceArray); var i: Integer; begin @@ -1496,6 +1546,24 @@ begin HzF := JsonGetFloat(Json, 'frac', -1); if (HzF >= 0) and Assigned(FOnBeaconSeed) then FOnBeaconSeed(HzF); end + else if Cmd = 'set_rx_gain_mode' then + begin + Idx := JsonGetInt(Json, 'mode', FRxGainMode); // 0=man,1=fast,2=slow,3=hybrid + FStateLock.Enter; FRxGainMode := Idx; FStateLock.Leave; + if Assigned(FOnRxGainMode) then FOnRxGainMode(Idx); + end + else if Cmd = 'set_rx_gain' then + begin + DB := JsonGetInt(Json, 'db', FRxGainDb); // ручной gain 0..73 дБ + FStateLock.Enter; FRxGainDb := DB; FStateLock.Leave; + if Assigned(FOnRxGain) then FOnRxGain(DB); + end + else if Cmd = 'set_rx_mute' then + begin + On_ := JsonGetBool(Json, 'on', FRxMuteOn); + FStateLock.Enter; FRxMuteOn := On_; FStateLock.Leave; + if Assigned(FOnRxMute) then FOnRxMute(On_); + end else if Cmd = 'freq_a' then begin HzF := JsonGetFloat(Json, 'hz', FFreq); @@ -1718,7 +1786,9 @@ begin '"fmstep_idx":%d,"devices":%s,"rate_presets":%s,"bands":%s,' + '"freq_min":%.0f,"freq_max":%.0f,' + '"beacon_visible":%s,"beacon_on":%s,"beacon_text":"%s",' + - '"beacon_ref_hz":%.0f,"beacon_track_hz":%.0f}', + '"beacon_ref_hz":%.0f,"beacon_track_hz":%.0f,' + + '"rf_agc_visible":%s,"rx_gain_mode":%d,"rx_gain_db":%d,' + + '"rx_mute_visible":%s,"rx_mute_on":%s}', [FFreq, FVfoB, FActiveVfo, FMode, MODE_N[FMode mod 8], FFilterIdx, FFilterBW, @@ -1751,7 +1821,10 @@ begin FFreqMin, FFreqMax, BoolToStr(FBeaconVisible, 'true', 'false'), BoolToStr(FBeaconOn, 'true', 'false'), - JsonEscape(FBeaconText), FBeaconRefHz, FBeaconTrackHz + JsonEscape(FBeaconText), FBeaconRefHz, FBeaconTrackHz, + BoolToStr(FRfAgcVisible, 'true', 'false'), FRxGainMode, FRxGainDb, + BoolToStr(FRxMuteVisible, 'true', 'false'), + BoolToStr(FRxMuteOn, 'true', 'false') ], FS); finally FStateLock.Leave; diff --git a/doc/PLUTO_INTEGRATION_PLAN.md b/doc/PLUTO_INTEGRATION_PLAN.md index 5765b31..60f20a4 100644 --- a/doc/PLUTO_INTEGRATION_PLAN.md +++ b/doc/PLUTO_INTEGRATION_PLAN.md @@ -260,8 +260,12 @@ end; - ✅ RX gain: блок `RF AGC` (десктоп) — кнопки FAST/SLOW/HYB/MAN (4 режима AD9361, подтверждены на железе `gain_control_mode_available`) + слайдер manual 0–73 дБ при - `Caps.HasHWGain`; персист per-device. WDSP AGC поверх. TODO: проброс RF-AGC в web - (сейчас в web только отображение, без управления). + `Caps.HasHWGain`; персист per-device. WDSP AGC поверх. +- ✅ **RF-AGC в web** (управление): дропдаун режима RFagc-F/S/H/M + слайдер GAIN + 0–73 дБ (активен только в MAN), виден при `BackendCaps.HasHWGain` (Pluto подключён). + Команды `set_rx_gain_mode`/`set_rx_gain` → `SetRxGainMode`/`SetRxGain`; state-поля + `rf_agc_visible`/`rx_gain_mode`/`rx_gain_db`. GUI-web-сервер + headless-демон. Собрано, + live-проверка на железе за пользователем. - Телеметрия: S-meter (WDSP) — для обоих. fwd/SWR/supply/PLL/overload — скрыть при `not Caps.HasPA`. ✅ RSSI/температура Pluto: `ReadTelemetry` (backend) → `ServicePlutoTelemetry` (контроллер, опрос ~2 Гц с фильтром выбросов) → поле 3 @@ -309,7 +313,12 @@ end; - ✅ RX gain / hw-AGC, транспорт (USB/IP), persist per-device по synthetic-MAC. - ✅ Имя платы (Pluto/LibreSDR + serial) и телеметрия в статус-бар: RSSI + **две температуры** (трансивер AD9361 + Zynq SoC `xadc`, формат `Temp 37/53C`). -- TODO: XO ppm-калибровка, buffer/timeout-контролы. +- TODO: **XO ppm-калибровка** — корень частотной ошибки опорника Pluto. Сейчас + обходится вручную через `TxLOOffset` QO-100-слота (подстройка под фактический + uplink, см. Фаза 5). Правильный фикс — `xo_correction` AD9361 (правит RX **и** TX + в ppm на всех частотах разом) или внешний 40 МГц GPSDO-реф (золотой стандарт для + QO-100). На 2.4 ГГц 1 ppm ≈ 2.4 кГц, поэтому TX без GPSDO плывёт с прогревом. +- TODO: buffer/timeout-контролы. **Фаза 4 — TX-тракт Pluto** ✅ - ✅ `SendDUCIQ` 24→16 + апсемпл 192k→device-rate + пейсинг (FIFO + push-поток, §4.6). @@ -319,9 +328,10 @@ end; - ✅ Управление мощностью через TX-аттенюацию (ADI-модель), маппинг **power-linear** + полная цифра WDSP (§4.8). Без PA → нет drive-байта/SWR/Alex/keyer. - ✅ **TX LO powerdown по PTT** (§4.8) — нет утечки несущей TX в RX-спектре в простое. -- TODO: проверка на железе на rate ≥1536k (ориентация SSB по Q-инверсии, уровни); - опц. headroom −6 дБ для FM (full-scale const-envelope → нелинейность ЦАП); опц. - зануление IQ на приёме. Опц. поддержка F5OEO-прошивки (PTT внешнего PA через +- ✅ **Проверено на железе** (передача QO-100 через спутник, downlink виден в + full-duplex self-monitor; SSB-ориентация и уровни в норме). +- TODO: опц. headroom −6 дБ для FM (full-scale const-envelope → нелинейность ЦАП); + опц. зануление IQ на приёме. Опц. поддержка F5OEO-прошивки (PTT внешнего PA через `hardwaregain=−40`) — отложено, нужно только под конкретное железо. **Фаза 5 — Режим QO-100** ✅ @@ -342,7 +352,9 @@ end; глушится на TX, как раньше); OFF → RX-аудио идёт на колонки и во время TX (свой downlink через транспондер, ~250 мс). Реализация: гейт `WDSPEngine` (`not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX)`) + флаг `FRxMuteOnTx` в контроллере, - персист в глобале (`rx_mute_on_tx`). Web — не сделано (пока вне области). + персист в глобале (`rx_mute_on_tx`). ✅ **В web**: toggle-кнопка `RX MUTE` рядом с + BCN, видна по `InQO100`, подсветка по `rx_mute_on`; команда `set_rx_mute` → + `SetRxMuteOnTx`. GUI-web-сервер + headless-демон. - ✅ **Bandplan-оверлей NB-транспондера** (`BandPlanOverlay.pas`): полоска внизу спектра (над ruler) с сегментами CW ONLY / NB DIGI / DIGI / SSB / MIXED MODES + маяки CW/PSK/EXP. Модель `VfoOverlay` (кэш-битмап + const-alpha), CPU и GL, виден @@ -351,7 +363,10 @@ end; * **Индикатор «не громче маяка»** — сравнение уровня своего downlink с маяком (этикет QO-100); амплитуда маяка уже известна декодеру. * RIT/XIT-клариффайер — опц., на геостационаре нужен редко. -- TODO: проверка на железе. +- ✅ **Проверено на железе** (QSO через QO-100: TX uplink 2400 + RX downlink 739.5 МГц, + full-duplex self-monitor). Нюанс калибровки: `TxLOOffset` пришлось подстроить руками + под фактический uplink — это ошибка опорника Pluto (TCXO), а не геометрия (геом. + offset точно 8089.5 МГц). Постоянное решение — XO ppm / GPSDO, см. Фаза 3 TODO. - ОТВЕРГНУТО: TX-маркер на RX-спектре в DUP — в обычной работе RX=TX (один VFO), маркер ложится на RX-курсор, информации не даёт; полезен только в split (редко). @@ -359,8 +374,8 @@ end; - ✅ Web/CAT-фронтенды (команды уже агностичны) + гейтинг полей; демон `ewsdrd.lpr` (discovery/connect Pluto headless, beacon-lock @10 Гц); кросс-платформенная сборка с/без libiio. -- TODO: проброс в web того, чего там ещё нет — RF-AGC (gain/режимы) и QO-100 - `RX MUTE` (self-monitor). Доки актуализируются по ходу. +- ✅ Проброс в web RF-AGC (gain/режимы) и QO-100 `RX MUTE` (self-monitor) — см. + §4.7 / Фаза 5. Доки актуализируются по ходу. **Сопутствующие исправления (найдены при тесте Pluto VHF/UHF FM, но баги общие):** - ✅ Восстановление per-band FM-настроек на старте (`Settings.LoadDevice`): bulk- diff --git a/ewsdr.lpi b/ewsdr.lpi index 0ac3d73..83ebba2 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -17,9 +17,9 @@ - + - + @@ -46,6 +46,7 @@ + diff --git a/ewsdrd.lpr b/ewsdrd.lpr index 75f5384..1af8123 100644 --- a/ewsdrd.lpr +++ b/ewsdrd.lpr @@ -168,6 +168,8 @@ begin 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) — на демон-хост (маршалит в главный поток).