feat: QO-100 self-monitor (RX MUTE) + gate beacon/RX-mute buttons to QO-100

Add a per-operator toggle to hear your own downlink off the satellite
while transmitting (full-duplex self-monitor), and restrict QO-100-only
controls to the QO-100 mode.

- WDSPEngine: new MonitorRXDuringTX flag; audio gate becomes
  `not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX)` so RX audio
  can pass to speakers during TX in full-duplex.
- Controller: FRxMuteOnTx (default True = mute RX on TX), SetRxMuteOnTx/
  ApplyRxMuteToEngine, rfRxMuteOnTx notify; new InQO100 predicate
  (Pluto + full-duplex XVTR slot) as the single gate for QO-100 UI.
- Settings: global RxMuteOnTx (rx_mute_on_tx, default True), persisted.
- MainForm: new RX MUTE button in TX panel next to DUP, visible only in
  QO-100; BEACON button visibility tightened from "any transverter" to
  InQO100 so it no longer shows in plain VHF/UHF modes.
- doc: plan updated (RF-AGC done, two temps, TX/RX locked split,
  self-monitor, phase 6 done; remaining TODOs incl. web wiring).

Web intentionally left unchanged for now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 19:50:30 +03:00
co-authored by Claude Opus 4.8
parent 6375092741
commit 3bd37b956e
5 changed files with 117 additions and 11 deletions
+35 -1
View File
@@ -237,6 +237,7 @@ type
BtnCTun: TFlatButton;
BtnBeacon: TFlatButton; // QO-100 beacon lock (видим только на Pluto в трансвертере)
BtnDUP: TFlatButton;
BtnRxMute: TFlatButton; // QO-100 self-monitor: RX-аудио на TX (видим только Pluto+full-duplex)
PanelFMSQ: TPanel;
BtnFMSQ: TFlatButton;
TrkFMSQ: TFlatSlider;
@@ -387,6 +388,8 @@ type
procedure UpdateBeaconButton;
procedure BtnDUPClick(Sender: TObject);
procedure ApplyDUP(Active: Boolean);
procedure BtnRxMuteClick(Sender: TObject);
procedure UpdateRxMuteButton; // видимость (Pluto+full-duplex) + стиль кнопки RX MUTE
procedure BtnTUNClick(Sender: TObject);
procedure ApplyTUN(Active: Boolean);
procedure UpdateFilterButtons;
@@ -1212,9 +1215,12 @@ begin
// openHPSDR смысла не имеет (нет дрейфующего LNB → прячем).
if BtnBeacon <> nil then
begin
BtnBeacon.Visible := FController.IsPluto and (FController.FCurrentXvtr >= 0);
// Только Pluto + full-duplex транспондер (QO-100) — beacon-lock завязан
// на дрейф LNB, в обычных диапазонах/трансвертерах смысла нет.
BtnBeacon.Visible := FController.InQO100;
UpdateBeaconButton;
end;
UpdateRxMuteButton; // RX MUTE — только Pluto + full-duplex (QO-100)
// Wideband-вид + web-зеркало xvtr (ранее в обёртках Activate/Deactivate).
// rfXvtr эмитят ТОЛЬКО ActivateXvtr/DeactivateXvtr — состояние финальное.
UpdateWidebandFrequencyView;
@@ -1313,6 +1319,8 @@ begin
PbWaterfall.Invalidate;
end;
end;
rfRxMuteOnTx:
UpdateRxMuteButton; // QO-100 self-monitor toggle: подсветка кнопки RX MUTE
rfWfAGC:
FSpecView.WfAGCEnabled := FController.FWfAGCEnabled;
rfWfNF:
@@ -1707,6 +1715,15 @@ begin
BtnDUP.Tag := 0;
StyleButton(BtnDUP, FController.FDisplayDuplex);
// QO-100 self-monitor: рядом с DUP, col1. Виден только на Pluto в full-duplex
// транспондере. ON (подсвечена, умолч.) = RX-аудио глушится на TX; OFF = слышим
// свой downlink со спутника.
BtnRxMute := MakeBtn(PanelTX, 'RX MUTE',
LeftPanelButtonLeft(LEFT_W, 4, 1), 44,
LeftPanelButtonWidth(LEFT_W, 4, 1), BTN_SM, BtnRxMuteClick);
BtnRxMute.Visible := False;
StyleButton(BtnRxMute, FController.FRxMuteOnTx);
LblDrv := TLabel.Create(Self);
LblDrv.Parent := PanelTX; LblDrv.Left := 4; LblDrv.Top := 77;
LblDrv.Caption := 'DRV'; LblDrv.Font.Color := CLR_TEXTDIM;
@@ -3697,6 +3714,7 @@ begin
// Отдельный Pluto RF-AGC блок под AGC: показываем и сдвигаем панели ниже.
LayoutLeftPanel(Caps.HasHWGain);
if Caps.HasHWGain then UpdateRxGainUI;
UpdateRxMuteButton; // RX MUTE — видимость по Pluto + full-duplex (если QO-100 уже активен)
end;
procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice);
@@ -4032,6 +4050,22 @@ begin
ApplyDUP(not FController.FDisplayDuplex);
end;
procedure TMainForm.BtnRxMuteClick(Sender: TObject);
begin
FController.SetRxMuteOnTx(not FController.FRxMuteOnTx); // рендер через rfRxMuteOnTx
end;
procedure TMainForm.UpdateRxMuteButton;
// Кнопка RX MUTE видна только на Pluto в full-duplex транспондере (QO-100).
// ON (подсвечена) = RX-аудио глушится на TX; OFF = self-monitor (слышим себя).
var Vis: Boolean;
begin
if BtnRxMute = nil then Exit;
Vis := FController.InQO100;
BtnRxMute.Visible := Vis;
if Vis then StyleButton(BtnRxMute, FController.FRxMuteOnTx);
end;
// ---------------------------------------------------------------------------
// TUN — Thetis-style тон: WDSP TXA PostGen + MOX с отдельным уровнем Drive
// ---------------------------------------------------------------------------
+37 -1
View File
@@ -67,7 +67,7 @@ type
rfVolume, rfMute, rfDrive, rfAtten,
rfBand, rfXvtr, rfBandRestore, // rfBandRestore: явная смена диапазона (сброс Wf-avg в UI)
rfCenterFreq, rfSpan, rfSampleRate,
rfCTun, rfDuplex,
rfCTun, rfDuplex, rfRxMuteOnTx, // rfRxMuteOnTx: QO-100 self-monitor toggle
rfRunning, rfConnected, rfTransmitting, rfTuning,
rfNR, rfNB, rfSNB, rfANF,
rfWfAGC, rfWfNF, rfDither, rfRandom,
@@ -217,6 +217,9 @@ type
FTuning: Boolean;
FSplitTxB: Boolean;
FDisplayDuplex: Boolean;
// QO-100 full-duplex self-monitor: True — RX-аудио глушится на TX (по умолч.),
// False — слышим свой downlink со спутника при передаче. Кнопка RX MUTE.
FRxMuteOnTx: Boolean;
FHWPTTActive: Boolean;
FHWPTTStartedTX: Boolean;
FWebClientActive: Boolean; // выставляет web-слой (connect/disconnect клиента)
@@ -496,6 +499,8 @@ type
// Отображение / приём
procedure SetCTun(On_: Boolean);
procedure SetDuplex(On_: Boolean);
procedure SetRxMuteOnTx(On_: Boolean); // QO-100 self-monitor (кнопка RX MUTE)
procedure ApplyRxMuteToEngine; // FRxMuteOnTx → FDSPEngine.MonitorRXDuringTX
procedure SetWfAGC(On_: Boolean);
procedure SetWfNF(On_: Boolean);
@@ -552,6 +557,8 @@ type
function BandPlan: TBandPlanArray;
// True если активный бэкенд — Pluto (VHF/UHF band-план вместо HF).
function IsPluto: Boolean;
// True — активен QO-100 (Pluto + full-duplex XVTR-слот); гейт UI beacon/RX MUTE.
function InQO100: Boolean;
// Человекочитаемое имя подключённой платы для статус-строки (UI/Web).
// HPSDR → тип платы; Pluto/LibreSDR → hw_model + серийник (если есть).
function BoardDisplayName: string;
@@ -593,6 +600,7 @@ begin
FMode := 1; FFilter := 5; FFilterBW := 2700;
FAGCMode := 1; FAGCTop := 90;
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
FVolume := 70; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
FCurrentBand := 5; FCurrentXvtr := -1;
@@ -1552,6 +1560,7 @@ begin
Result.WidebandFill := FWidebandFill;
Result.SpectrumFill := FSpectrumFill;
Result.DisplayDuplex := FDisplayDuplex;
Result.RxMuteOnTx := FRxMuteOnTx;
Result.AudioOutDevice := FAudioOutDevName;
Result.AudioInDevice := FAudioInDevName;
Result.SendAudioToRadio := FSendAudioToRadio;
@@ -2371,6 +2380,21 @@ begin
Changed(rfDuplex);
end;
procedure TRadioController.ApplyRxMuteToEngine;
begin
// Монитор имеет смысл только в full-duplex (KeepRXDuringTX); сам гейт в движке
// это учитывает. Здесь просто транслируем инверсию флага RX MUTE.
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.MonitorRXDuringTX := not FRxMuteOnTx;
end;
procedure TRadioController.SetRxMuteOnTx(On_: Boolean);
begin
FRxMuteOnTx := On_;
ApplyRxMuteToEngine;
Changed(rfRxMuteOnTx);
end;
procedure TRadioController.SetWfAGC(On_: Boolean);
begin FWfAGCEnabled := On_; Changed(rfWfAGC); end;
@@ -2783,6 +2807,8 @@ begin
FNetwork.SetSpeakerAudio(FSendAudioToRadio);
// RX hw-gain в бэкенд (Pluto). HPSDR — no-op.
ApplyRxGainToBackend;
// QO-100 self-monitor: RX-аудио на TX (full-duplex). По умолчанию заглушено.
ApplyRxMuteToEngine;
SendDUCSpecificFromSettings;
// Применяем TX-цепь (фильтр, mic gain, EQ, leveler, ALC, comp, ...).
ApplyTXSettingsToDSP;
@@ -2949,6 +2975,7 @@ begin
FWidebandFill := FLoadedGlobal.WidebandFill;
FSpectrumFill := FLoadedGlobal.SpectrumFill;
FDisplayDuplex := FLoadedGlobal.DisplayDuplex;
FRxMuteOnTx := FLoadedGlobal.RxMuteOnTx;
// Настройки дисплея/сетки + DSP display + аудио-устройства
if FLoadedGlobal.FFTSize > 0 then
begin
@@ -3177,6 +3204,15 @@ begin
Result := (FNetwork <> nil) and (FNetwork.Caps.Kind = bkPluto);
end;
function TRadioController.InQO100: Boolean;
// True — активен full-duplex транспондер (QO-100): Pluto + выбран XVTR-слот с
// флагом FullDuplexTx. Гейт для UI-элементов, осмысленных только в этом режиме
// (beacon-lock, RX MUTE self-monitor).
begin
Result := IsPluto and (FCurrentXvtr >= 0)
and FXvtrSettings.Entries[FCurrentXvtr].FullDuplexTx;
end;
function TRadioController.BoardDisplayName: string;
var
Dev: THPSDRDevice;
+4
View File
@@ -261,6 +261,7 @@ type
WidebandFill: Boolean; // True = fill wideband spectrum with gradient
SpectrumFill: Boolean; // True = fill main spectrum under the curve with gradient
DisplayDuplex: Boolean; // DUP: при TX показывать RX-водопад, TX-фильтр overlay'ем
RxMuteOnTx: Boolean; // QO-100 self-monitor: True=RX-аудио глушится на TX (умолч.)
// --- Display FPS ---
DisplayFPS: Integer; // spectrum/waterfall timer fps (1..100)
// --- Theme ---
@@ -448,6 +449,7 @@ begin
G.WidebandFill := False;
G.SpectrumFill := True;
G.DisplayDuplex := False;
G.RxMuteOnTx := True;
G.DisplayFPS := 60;
G.FreqMhzDigits := 3;
// CAT defaults
@@ -671,6 +673,7 @@ begin
G.WidebandFill := JB(GObj,'wideband_fill',False);
G.SpectrumFill := JB(GObj,'spectrum_fill',True);
G.DisplayDuplex := JB(GObj,'display_duplex',False);
G.RxMuteOnTx := JB(GObj,'rx_mute_on_tx',True);
G.DisplayFPS := JI(GObj,'display_fps',60);
G.LightTheme := JB(GObj,'light_theme',False);
G.FreqMhzDigits := EnsureRange(JI(GObj,'freq_mhz_digits',3), 3, 5);
@@ -774,6 +777,7 @@ begin
JW(O,'wideband_fill',G.WidebandFill);
JW(O,'spectrum_fill',G.SpectrumFill);
JW(O,'display_duplex',G.DisplayDuplex);
JW(O,'rx_mute_on_tx',G.RxMuteOnTx);
JW(O,'display_fps',G.DisplayFPS);
JW(O,'light_theme',G.LightTheme);
JW(O,'freq_mhz_digits',G.FreqMhzDigits);
+14 -1
View File
@@ -223,6 +223,10 @@ type
// (мы выяснили на практике что стоп/старт RXA добавляет задержки на
// Windows без видимого выигрыша).
FKeepRXDuringTX: Boolean;
// Полный дуплекс QO-100: пропускать RX-аудио на колонки и во время TX (слышим
// свой downlink через транспондер). Имеет смысл только при FKeepRXDuringTX.
// По умолчанию False — обычное поведение (RX-аудио глушится на TX).
FMonitorRXDuringTX: Boolean;
// Дедлайн (GetTickCount64), до которого RX-аудио не пишется в FOnAudio.
// Используется на Windows для маскировки «хвоста» TX leakage (radio
// FPGA TX-buffer + PA slew-down → RX1 IQ → audio) после MOX-off.
@@ -461,6 +465,10 @@ type
// RX-анализатор продолжает получать IQ. Должен ставиться UI ДО SetTXRun.
property KeepRXDuringTX: Boolean read FKeepRXDuringTX
write FKeepRXDuringTX;
// Full-duplex self-monitor: при TX пропускать RX-аудио на колонки (QO-100 —
// слышим себя со спутника). Действует только вместе с KeepRXDuringTX.
property MonitorRXDuringTX: Boolean read FMonitorRXDuringTX
write FMonitorRXDuringTX;
// Активный display ID — RX_DISP_ID или TX_DISP_ID (зависит от FTXActive).
// SpectrumView/Waterfall должны читать пиксели именно из этого ID.
property ActiveDisplayID: Integer read FActiveDisplayID;
@@ -788,6 +796,7 @@ begin
FTXMicTail := 0;
FTXActive := False;
FKeepRXDuringTX := False;
FMonitorRXDuringTX := False;
FPostTXMuteUntil := 0;
FActiveDisplayID := RX_DISP_ID;
FTXMicSource := txmsRadio;
@@ -1302,7 +1311,11 @@ begin
// через unsigned-сравнение, GetTickCount64 не зовётся когда дедлайн = 0.
if (FPostTXMuteUntil <> 0) and (GetTickCount64 >= FPostTXMuteUntil) then
FPostTXMuteUntil := 0;
if Assigned(FOnAudio) and not FMuted and not FTXActive
// Full-duplex self-monitor (QO-100): при FMonitorRXDuringTX не глушим RX-аудио
// на TX — слышим свой downlink со спутника. Иначе (по умолчанию) глушим, как
// Thetis/piHPSDR при не-duplex MOX. FPostTXMuteUntil-хвост уважаем в любом случае.
if Assigned(FOnAudio) and not FMuted
and (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
and (FPostTXMuteUntil = 0) then
begin
for i := 0 to FAudioBufSize - 1 do
+27 -8
View File
@@ -258,12 +258,16 @@ end;
### 4.7 Усиление / телеметрия
- RX gain: контрол manual gain (073 дБ) / режим hw-AGC при `Caps.HasHWGain`
(можно расширить UI step-atten). WDSP AGC поверх.
- 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 только отображение, без управления).
- Телеметрия: S-meter (WDSP) — для обоих. fwd/SWR/supply/PLL/overload — скрыть при
`not Caps.HasPA`. ✅ RSSI/температура Pluto: `ReadTelemetry` (backend) →
`ServicePlutoTelemetry` (контроллер, опрос ~2 Гц с фильтром выбросов) → поле 3
статус-бара (Temp/RSSI вместо Supply, т.к. у Pluto нет PA).
статус-бара (Temp/RSSI вместо Supply, т.к. у Pluto нет PA).**Две температуры**:
трансивер AD9361 (`ad9361-phy temp0`) + Zynq SoC/FPGA (`xadc temp0`,
`(raw+offset)*scale/1000`) — формат `Temp 37/53C` в десктопе и web.
### 4.8 TX-мощность / drive (Pluto)
@@ -303,7 +307,8 @@ end;
**Фаза 3 — Управление/калибровка**
- ✅ 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`).
- TODO: XO ppm-калибровка, buffer/timeout-контролы.
**Фаза 4 — TX-тракт Pluto** ✅
@@ -329,11 +334,25 @@ end;
- ✅ Отдельная страница «QO-100» в SettingsForm (LNB LO / транспондер-offset /
границы downlink / RX gain / TX-потолок); зарезервирован последний XVTR-слот
(`QO100_SLOT`), не засоряет таблицу трансвертеров openHPSDR.
- TODO: проверка на железе.
- **TX↔RX связка с фиксированным офсетом**: TX-VFO в downlink-домене,
`XvtrTranslateTX` = `visible TxLOOffset` (≈8089.5 МГц) — крутишь приём, аплинк
едет следом, офсет держится сам (через общий VFO). Split — через VFO-B.
- ✅ **Аудио self-monitor** («слышу себя со спутника»): кнопка `RX MUTE` в TX-панели
рядом с DUP — видна только Pluto + full-duplex (QO-100). По умолчанию ON (RX-аудио
глушится на TX, как раньше); OFF → RX-аудио идёт на колонки и во время TX (свой
downlink через транспондер, ~250 мс). Реализация: гейт `WDSPEngine` (`not FTXActive
or (FKeepRXDuringTX and FMonitorRXDuringTX)`) + флаг `FRxMuteOnTx` в контроллере,
персист в глобале (`rx_mute_on_tx`). Web — не сделано (пока вне области).
- TODO: проверка на железе; RIT/XIT-клариффайер (опц., на геостационаре редко нужен);
TX-маркер на RX-спектре в DUP (визуальный self-monitor — `FSpecView.TXMode` сейчас
в DUP выключен).
**Фаза 6 — Полировка**
- Web/CAT-фронтенды (команды уже агностичны) + гейтинг полей; демон `ewsdrd.lpr`
(discovery/connect Pluto headless); кросс-платформенная сборка с/без libiio; доки.
**Фаза 6 — Полировка** ✅ (в основном)
- Web/CAT-фронтенды (команды уже агностичны) + гейтинг полей; демон `ewsdrd.lpr`
(discovery/connect Pluto headless, beacon-lock @10 Гц); кросс-платформенная сборка
с/без libiio.
- TODO: проброс в web того, чего там ещё нет — RF-AGC (gain/режимы) и QO-100
`RX MUTE` (self-monitor). Доки актуализируются по ходу.
**Сопутствующие исправления (найдены при тесте Pluto VHF/UHF FM, но баги общие):**
- ✅ Восстановление per-band FM-настроек на старте (`Settings.LoadDevice`): bulk-