feat: web RF-AGC + QO-100 RX MUTE controls (Pluto)

Прокинул в 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>
This commit is contained in:
2026-06-24 11:02:34 +03:00
co-authored by Claude Opus 4.8
parent 8bb8714a25
commit a885488243
7 changed files with 163 additions and 15 deletions
+3
View File
@@ -879,6 +879,9 @@ begin
FWebServer.OnXvtrBand := FWebAdapter.OnXvtrBand; FWebServer.OnXvtrBand := FWebAdapter.OnXvtrBand;
FWebServer.OnBeacon := FWebAdapter.OnBeacon; FWebServer.OnBeacon := FWebAdapter.OnBeacon;
FWebServer.OnBeaconSeed := FWebAdapter.OnBeaconSeed; FWebServer.OnBeaconSeed := FWebAdapter.OnBeaconSeed;
FWebServer.OnRxGainMode := FWebAdapter.OnRxGainMode;
FWebServer.OnRxGain := FWebAdapter.OnRxGain;
FWebServer.OnRxMute := FWebAdapter.OnRxMute;
FWebServer.OnConnect := WebOnConnect; FWebServer.OnConnect := WebOnConnect;
FWebServer.OnDiscoverDev := WebOnDiscover; FWebServer.OnDiscoverDev := WebOnDiscover;
FWebServer.OnDisconnectDev := WebOnDisconnect; FWebServer.OnDisconnectDev := WebOnDisconnect;
+41
View File
@@ -68,6 +68,9 @@ type
procedure SyncFMStep; procedure SyncFMStep;
procedure SyncBeacon; procedure SyncBeacon;
procedure SyncBeaconSeed; procedure SyncBeaconSeed;
procedure SyncRxGainMode;
procedure SyncRxGain;
procedure SyncRxMute;
function BeaconStatusText: string; // компактный статус (как десктоп поле 7) function BeaconStatusText: string; // компактный статус (как десктоп поле 7)
public public
constructor Create(AController: TRadioController; AServer: TWebServer; constructor Create(AController: TRadioController; AServer: TWebServer;
@@ -103,6 +106,9 @@ type
// QO-100 beacon: вкл/выкл лок и наведение по клику в спектре (frac 0..1). // QO-100 beacon: вкл/выкл лок и наведение по клику в спектре (frac 0..1).
procedure OnBeacon(On_: Boolean); procedure OnBeacon(On_: Boolean);
procedure OnBeaconSeed(Frac: Double); procedure OnBeaconSeed(Frac: Double);
procedure OnRxGainMode(Mode: Integer);
procedure OnRxGain(Db: Integer);
procedure OnRxMute(On_: Boolean);
procedure OnClientActiveChanged(Active: Boolean); procedure OnClientActiveChanged(Active: Boolean);
// TX-mic от web-клиента → WDSP. Вызывается из WS-потока; без маршалинга — // TX-mic от web-клиента → WDSP. Вызывается из WS-потока; без маршалинга —
// подача в движок thread-safe (как OnMicPacket/OnDDCIQ контроллера). // подача в движок thread-safe (как OnMicPacket/OnDDCIQ контроллера).
@@ -373,6 +379,33 @@ begin
FController.BeaconSeedAtHz(Hz); FController.BeaconSeedAtHz(Hz);
end; 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); procedure TWebAdapter.OnClientActiveChanged(Active: Boolean);
// Из потока веб-сервера при connect/disconnect клиента. Простая запись флага — // Из потока веб-сервера при connect/disconnect клиента. Простая запись флага —
// ядро (SetMOX, в т.ч. HWPTT-путь) выбирает по нему mic-source. Без маршалинга. // ядро (SetMOX, в т.ч. HWPTT-путь) выбирает по нему mic-source. Без маршалинга.
@@ -512,6 +545,14 @@ begin
FController.BeaconRefHz, FController.BeaconRefHz,
FController.BeaconTrackedFreqHz); 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( FServer.PushSpectrum(
FController.FSpectrumBuf, 1024, FController.FSpectrumBuf, 1024,
FController.FWaterfallBuf, FController.FWaterfallBuf,
+14 -1
View File
@@ -204,6 +204,13 @@ begin
'<button id="anfBtn">ANF</button>' + '<button id="anfBtn">ANF</button>' +
'<button id="nrBtn">NR</button>' + '<button id="nrBtn">NR</button>' +
'<button id="bcnBtn" title="QO-100 beacon lock" style="display:none">BCN</button>' + '<button id="bcnBtn" title="QO-100 beacon lock" style="display:none">BCN</button>' +
'<button id="rxMuteBtn" title="QO-100 RX self-monitor mute on TX" style="display:none">RX MUTE</button>' +
// Pluto RF-AGC (hw gain): режим FAST/SLOW/HYB/MAN + ручной gain (активен в MAN). Виден только на Pluto.
'<select id="rfagcSel" title="Pluto RF AGC mode" style="display:none">' +
'<option value="1">RFagc-F</option><option value="2" selected>RFagc-S</option>' +
'<option value="3">RFagc-H</option><option value="0">RFagc-M</option>' +
'</select>' +
'<div class="sl-g" id="rfgainG" style="display:none"><span class="sl-lb">GAIN</span><input type="range" id="rfgainSl" min="0" max="73" value="40"><span class="sl-val" id="rfgainVal">40dB</span></div>' +
'<span class="sl-sep">|</span>' + '<span class="sl-sep">|</span>' +
'<button id="devBtn" title="Devices">DEV</button>' + '<button id="devBtn" title="Devices">DEV</button>' +
'<button id="startStopBtn" class="on">START</button>' + '<button id="startStopBtn" class="on">START</button>' +
@@ -290,6 +297,8 @@ begin
'function updSnbBtn(){sw("snbBtn",!!cur.snb);}' + 'function updSnbBtn(){sw("snbBtn",!!cur.snb);}' +
'function updAnfBtn(){sw("anfBtn",!!cur.anf);}' + '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 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"];' + '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("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("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("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("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);};' + '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());}' + 'function isPending(field){return !!(pendingCmds[field]&&pendingCmds[field]>Date.now());}' +
// Маппинг: server JSON field → pend key // Маппинг: 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){' + 'function applyState(msg){' +
'Object.keys(msg).forEach(function(k){var pk=PEND_MAP[k];if(!pk||!isPending(pk))cur[k]=msg[k];});' + '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("nr"))updNrBtn();if(!isPending("nb"))updNbBtn();' +
'if(!isPending("snb"))updSnbBtn();if(!isPending("anf"))updAnfBtn();' + 'if(!isPending("snb"))updSnbBtn();if(!isPending("anf"))updAnfBtn();' +
'if(!isPending("beacon"))updBcnBtn();' + 'if(!isPending("beacon"))updBcnBtn();' +
'if(!isPending("rxmute"))updRxMute();updRfAgc();' +
'if(!isPending("run"))updRunBtn(!!cur.running);' + 'if(!isPending("run"))updRunBtn(!!cur.running);' +
'if(!isPending("mox"))updMoxBtn(!!cur.transmitting);' + 'if(!isPending("mox"))updMoxBtn(!!cur.transmitting);' +
'if(!isPending("tun"))updToneBtn(!!cur.tuning);' + 'if(!isPending("tun"))updToneBtn(!!cur.tuning);' +
+75 -2
View File
@@ -154,6 +154,10 @@ type
// QO-100 beacon lock: вкл/выкл лок и наведение (клик по маяку, доля 0..1 по ширине). // QO-100 beacon lock: вкл/выкл лок и наведение (клик по маяку, доля 0..1 по ширине).
TWebCmdBeacon = procedure(On_: Boolean) of object; TWebCmdBeacon = procedure(On_: Boolean) of object;
TWebCmdBeaconSeed = procedure(Frac: Double) 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-клиента изменилась (подключился/отключился последний клиент). // Активность web-клиента изменилась (подключился/отключился последний клиент).
// Хозяин зеркалит это в ядро (FController.FWebClientActive) для выбора mic-source. // Хозяин зеркалит это в ядро (FController.FWebClientActive) для выбора mic-source.
TWebClientActiveEvent = procedure(Active: Boolean) of object; TWebClientActiveEvent = procedure(Active: Boolean) of object;
@@ -330,6 +334,18 @@ type
FOnBeacon: TWebCmdBeacon; FOnBeacon: TWebCmdBeacon;
FOnBeaconSeed: TWebCmdBeaconSeed; 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; FWebClientActive: Boolean;
FOnClientActiveChanged: TWebClientActiveEvent; FOnClientActiveChanged: TWebClientActiveEvent;
@@ -416,8 +432,14 @@ type
// QO-100 beacon lock: зеркало статуса в state (зовётся из PushState хозяина). // QO-100 beacon lock: зеркало статуса в state (зовётся из PushState хозяина).
procedure SetBeaconStatus(AVisible, AOn: Boolean; const AText: string; procedure SetBeaconStatus(AVisible, AOn: Boolean; const AText: string;
ARefHz, ATrackHz: Double); 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 OnBeacon: TWebCmdBeacon read FOnBeacon write FOnBeacon;
property OnBeaconSeed: TWebCmdBeaconSeed read FOnBeaconSeed write FOnBeaconSeed; 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 OnSpan: TWebCmdSpan read FOnSpan write FOnSpan;
property OnVolume: TWebCmdVolume read FOnVolume write FOnVolume; property OnVolume: TWebCmdVolume read FOnVolume write FOnVolume;
property OnWfAGC: TWebCmdWfAGC read FOnWfAGC write FOnWfAGC; property OnWfAGC: TWebCmdWfAGC read FOnWfAGC write FOnWfAGC;
@@ -565,6 +587,11 @@ begin
FBeaconText := ''; FBeaconText := '';
FBeaconRefHz := 0; FBeaconRefHz := 0;
FBeaconTrackHz := 0; FBeaconTrackHz := 0;
FRfAgcVisible := False;
FRxGainMode := 2; // slow_attack по умолчанию (как контроллер)
FRxGainDb := 40;
FRxMuteVisible := False;
FRxMuteOn := False;
SetLength(FXvtrBands, 0); SetLength(FXvtrBands, 0);
// Bootstrap-набор рейтов (до connect): хост перезапишет под бэкенд (HPSDR/Pluto). // Bootstrap-набор рейтов (до connect): хост перезапишет под бэкенд (HPSDR/Pluto).
SetRatePresets([48000, 96000, 192000, 384000, 768000, 1536000]); SetRatePresets([48000, 96000, 192000, 384000, 768000, 1536000]);
@@ -604,6 +631,29 @@ begin
end; end;
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); procedure TWebServer.SetDeviceList(const ADevices: TWebDeviceArray);
var i: Integer; var i: Integer;
begin begin
@@ -1496,6 +1546,24 @@ begin
HzF := JsonGetFloat(Json, 'frac', -1); HzF := JsonGetFloat(Json, 'frac', -1);
if (HzF >= 0) and Assigned(FOnBeaconSeed) then FOnBeaconSeed(HzF); if (HzF >= 0) and Assigned(FOnBeaconSeed) then FOnBeaconSeed(HzF);
end 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 else if Cmd = 'freq_a' then
begin begin
HzF := JsonGetFloat(Json, 'hz', FFreq); HzF := JsonGetFloat(Json, 'hz', FFreq);
@@ -1718,7 +1786,9 @@ begin
'"fmstep_idx":%d,"devices":%s,"rate_presets":%s,"bands":%s,' + '"fmstep_idx":%d,"devices":%s,"rate_presets":%s,"bands":%s,' +
'"freq_min":%.0f,"freq_max":%.0f,' + '"freq_min":%.0f,"freq_max":%.0f,' +
'"beacon_visible":%s,"beacon_on":%s,"beacon_text":"%s",' + '"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, [FFreq, FVfoB, FActiveVfo,
FMode, MODE_N[FMode mod 8], FMode, MODE_N[FMode mod 8],
FFilterIdx, FFilterBW, FFilterIdx, FFilterBW,
@@ -1751,7 +1821,10 @@ begin
FFreqMin, FFreqMax, FFreqMin, FFreqMax,
BoolToStr(FBeaconVisible, 'true', 'false'), BoolToStr(FBeaconVisible, 'true', 'false'),
BoolToStr(FBeaconOn, '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); ], FS);
finally finally
FStateLock.Leave; FStateLock.Leave;
+25 -10
View File
@@ -260,8 +260,12 @@ end;
- ✅ RX gain: блок `RF AGC` (десктоп) — кнопки FAST/SLOW/HYB/MAN (4 режима AD9361, - ✅ RX gain: блок `RF AGC` (десктоп) — кнопки FAST/SLOW/HYB/MAN (4 режима AD9361,
подтверждены на железе `gain_control_mode_available`) + слайдер manual 0–73 дБ при подтверждены на железе `gain_control_mode_available`) + слайдер manual 0–73 дБ при
`Caps.HasHWGain`; персист per-device. WDSP AGC поверх. TODO: проброс RF-AGC в web `Caps.HasHWGain`; персист per-device. WDSP AGC поверх.
(сейчас в web только отображение, без управления). - ✅ **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 — скрыть при - Телеметрия: S-meter (WDSP) — для обоих. fwd/SWR/supply/PLL/overload — скрыть при
`not Caps.HasPA`. ✅ RSSI/температура Pluto: `ReadTelemetry` (backend) → `not Caps.HasPA`. ✅ RSSI/температура Pluto: `ReadTelemetry` (backend) →
`ServicePlutoTelemetry` (контроллер, опрос ~2 Гц с фильтром выбросов) → поле 3 `ServicePlutoTelemetry` (контроллер, опрос ~2 Гц с фильтром выбросов) → поле 3
@@ -309,7 +313,12 @@ end;
- ✅ RX gain / hw-AGC, транспорт (USB/IP), persist per-device по synthetic-MAC. - ✅ RX gain / hw-AGC, транспорт (USB/IP), persist per-device по synthetic-MAC.
- ✅ Имя платы (Pluto/LibreSDR + serial) и телеметрия в статус-бар: RSSI + **две - ✅ Имя платы (Pluto/LibreSDR + serial) и телеметрия в статус-бар: RSSI + **две
температуры** (трансивер AD9361 + Zynq SoC `xadc`, формат `Temp 37/53C`). температуры** (трансивер 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** ✅ **Фаза 4 — TX-тракт Pluto** ✅
- ✅ `SendDUCIQ` 24→16 + апсемпл 192k→device-rate + пейсинг (FIFO + push-поток, §4.6). - ✅ `SendDUCIQ` 24→16 + апсемпл 192k→device-rate + пейсинг (FIFO + push-поток, §4.6).
@@ -319,9 +328,10 @@ end;
- ✅ Управление мощностью через TX-аттенюацию (ADI-модель), маппинг **power-linear** - ✅ Управление мощностью через TX-аттенюацию (ADI-модель), маппинг **power-linear**
+ полная цифра WDSP (§4.8). Без PA → нет drive-байта/SWR/Alex/keyer. + полная цифра WDSP (§4.8). Без PA → нет drive-байта/SWR/Alex/keyer.
- ✅ **TX LO powerdown по PTT** (§4.8) — нет утечки несущей TX в RX-спектре в простое. - ✅ **TX LO powerdown по PTT** (§4.8) — нет утечки несущей TX в RX-спектре в простое.
- TODO: проверка на железе на rate ≥1536k (ориентация SSB по Q-инверсии, уровни); - **Проверено на железе** (передача QO-100 через спутник, downlink виден в
опц. headroom 6 дБ для FM (full-scale const-envelope → нелинейность ЦАП); опц. full-duplex self-monitor; SSB-ориентация и уровни в норме).
зануление IQ на приёме. Опц. поддержка F5OEO-прошивки (PTT внешнего PA через - TODO: опц. headroom 6 дБ для FM (full-scale const-envelope → нелинейность ЦАП);
опц. зануление IQ на приёме. Опц. поддержка F5OEO-прошивки (PTT внешнего PA через
`hardwaregain=40`) — отложено, нужно только под конкретное железо. `hardwaregain=40`) — отложено, нужно только под конкретное железо.
**Фаза 5 — Режим QO-100** ✅ **Фаза 5 — Режим QO-100** ✅
@@ -342,7 +352,9 @@ end;
глушится на TX, как раньше); OFF → RX-аудио идёт на колонки и во время TX (свой глушится на TX, как раньше); OFF → RX-аудио идёт на колонки и во время TX (свой
downlink через транспондер, ~250 мс). Реализация: гейт `WDSPEngine` (`not FTXActive downlink через транспондер, ~250 мс). Реализация: гейт `WDSPEngine` (`not FTXActive
or (FKeepRXDuringTX and FMonitorRXDuringTX)`) + флаг `FRxMuteOnTx` в контроллере, 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`): полоска внизу - ✅ **Bandplan-оверлей NB-транспондера** (`BandPlanOverlay.pas`): полоска внизу
спектра (над ruler) с сегментами CW ONLY / NB DIGI / DIGI / SSB / MIXED MODES + спектра (над ruler) с сегментами CW ONLY / NB DIGI / DIGI / SSB / MIXED MODES +
маяки CW/PSK/EXP. Модель `VfoOverlay` (кэш-битмап + const-alpha), CPU и GL, виден маяки CW/PSK/EXP. Модель `VfoOverlay` (кэш-битмап + const-alpha), CPU и GL, виден
@@ -351,7 +363,10 @@ end;
* **Индикатор «не громче маяка»** — сравнение уровня своего downlink с маяком * **Индикатор «не громче маяка»** — сравнение уровня своего downlink с маяком
(этикет QO-100); амплитуда маяка уже известна декодеру. (этикет QO-100); амплитуда маяка уже известна декодеру.
* RIT/XIT-клариффайер — опц., на геостационаре нужен редко. * 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), - ОТВЕРГНУТО: TX-маркер на RX-спектре в DUP — в обычной работе RX=TX (один VFO),
маркер ложится на RX-курсор, информации не даёт; полезен только в split (редко). маркер ложится на RX-курсор, информации не даёт; полезен только в split (редко).
@@ -359,8 +374,8 @@ end;
- ✅ Web/CAT-фронтенды (команды уже агностичны) + гейтинг полей; демон `ewsdrd.lpr` - ✅ Web/CAT-фронтенды (команды уже агностичны) + гейтинг полей; демон `ewsdrd.lpr`
(discovery/connect Pluto headless, beacon-lock @10 Гц); кросс-платформенная сборка (discovery/connect Pluto headless, beacon-lock @10 Гц); кросс-платформенная сборка
с/без libiio. с/без libiio.
- TODO: проброс в web того, чего там ещё нет — RF-AGC (gain/режимы) и QO-100 - ✅ Проброс в web RF-AGC (gain/режимы) и QO-100 `RX MUTE` (self-monitor) — см.
`RX MUTE` (self-monitor). Доки актуализируются по ходу. §4.7 / Фаза 5. Доки актуализируются по ходу.
**Сопутствующие исправления (найдены при тесте Pluto VHF/UHF FM, но баги общие):** **Сопутствующие исправления (найдены при тесте Pluto VHF/UHF FM, но баги общие):**
- ✅ Восстановление per-band FM-настроек на старте (`Settings.LoadDevice`): bulk- - ✅ Восстановление per-band FM-настроек на старте (`Settings.LoadDevice`): bulk-
+3 -2
View File
@@ -17,9 +17,9 @@
<UseVersionInfo Value="True"/> <UseVersionInfo Value="True"/>
<AutoIncrementBuild Value="True"/> <AutoIncrementBuild Value="True"/>
<MinorVersionNr Value="9"/> <MinorVersionNr Value="9"/>
<BuildNr Value="214"/> <BuildNr Value="215"/>
</VersionInfo> </VersionInfo>
<MacroValues Count="26"> <MacroValues Count="27">
<Macro1 Name="LCLWidgetType" Value="qt6"/> <Macro1 Name="LCLWidgetType" Value="qt6"/>
<Macro2 Name="LCLWidgetType" Value="qt6"/> <Macro2 Name="LCLWidgetType" Value="qt6"/>
<Macro3 Name="LCLWidgetType" Value="qt6"/> <Macro3 Name="LCLWidgetType" Value="qt6"/>
@@ -46,6 +46,7 @@
<Macro24 Name="LCLWidgetType" Value="qt6"/> <Macro24 Name="LCLWidgetType" Value="qt6"/>
<Macro25 Name="LCLWidgetType" Value="qt6"/> <Macro25 Name="LCLWidgetType" Value="qt6"/>
<Macro26 Name="LCLWidgetType" Value="qt6"/> <Macro26 Name="LCLWidgetType" Value="qt6"/>
<Macro27 Name="LCLWidgetType" Value="qt6"/>
</MacroValues> </MacroValues>
<BuildModes> <BuildModes>
<Item Name="Debug" Default="True"/> <Item Name="Debug" Default="True"/>
+2
View File
@@ -168,6 +168,8 @@ begin
FWeb.OnMOX := @FWebAd.OnMOX; FWeb.OnTun := @FWebAd.OnTun; FWeb.OnMOX := @FWebAd.OnMOX; FWeb.OnTun := @FWebAd.OnTun;
FWeb.OnFMStep := @FWebAd.OnFMStep; FWeb.OnXvtrBand := @FWebAd.OnXvtrBand; FWeb.OnFMStep := @FWebAd.OnFMStep; FWeb.OnXvtrBand := @FWebAd.OnXvtrBand;
FWeb.OnBeacon := @FWebAd.OnBeacon; FWeb.OnBeaconSeed := @FWebAd.OnBeaconSeed; 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.OnWebMic := @FWebAd.OnMic;
FWeb.OnClientActiveChanged := @FWebAd.OnClientActiveChanged; FWeb.OnClientActiveChanged := @FWebAd.OnClientActiveChanged;
// Host-coupled (lifecycle) — на демон-хост (маршалит в главный поток). // Host-coupled (lifecycle) — на демон-хост (маршалит в главный поток).