feat: QO-100 mode — separate RX/TX LO + dedicated settings page

QO-100 reuses the transverter path (VFO/band-stack/beacon-lock untouched)
with a split LO for the geostationary transponder:
- TXvtrEntry += FullDuplexTx + TxLOOffset (persisted). Reserve the last
  XVTR slot (QO100_SLOT) for QO-100; migration restores the template when
  the slot is unset or has empty offsets (fixes old configs showing zeros).
- XvtrTranslateTX: TX LO = visible - TxLOOffset (no LNB LOError) for QO-100;
  identical to RX translate for normal transverters. Switched all TX-side
  translate calls (controller + MainForm).
- SetDuplex wired (live KeepRXDuringTX); auto display-duplex on QO-100 entry.
- New "QO-100" settings page (LNB LO / transponder offset / downlink edges /
  RX gain / TX ceiling + live RX-IF/TX readout); QO slot hidden from the
  transverter table. Reuses OnXvtrChange — no MainForm wiring needed.
- doc/PLUTO_INTEGRATION_PLAN.md: phase 5 done.

Builds clean (headless + qt6 GUI). Not yet verified on hardware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 15:30:23 +03:00
co-authored by Claude Opus 4.8
parent c6660891c0
commit 3fe5cf3fad
5 changed files with 292 additions and 47 deletions
+37 -7
View File
@@ -347,6 +347,10 @@ type
// активного трансвертора (LOOffset/LOError).
// ActiveTXFreqHz — текущая TX-частота (split/active VFO + FM-репитер сдвиг).
function XvtrTranslate(VisibleHz: Double): Double;
// XvtrTranslateTX — то же для ПЕРЕДАЧИ. Обычный трансвертер: = XvtrTranslate.
// QO-100 (FullDuplexTx): отдельный TX-LO (visible-TxLOOffset, без LOError —
// uplink-LO не подвержен дрейфу LNB).
function XvtrTranslateTX(VisibleHz: Double): Double;
function ActiveTXFreqHz: Double;
// Измерение маяка по display-FFT в окне ±WinHz вокруг ExpectOff (baseband):
// центроид лепестка → уточнённое смещение, «высота» пика над средним окна
@@ -629,7 +633,7 @@ procedure TRadioController.PushNetworkState;
begin
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz),
FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
@@ -979,6 +983,22 @@ begin
+ FXvtrSettings.Entries[FCurrentXvtr].LOError;
end;
function TRadioController.XvtrTranslateTX(VisibleHz: Double): Double;
begin
Result := VisibleHz;
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
if not FXvtrSettings.Entries[FCurrentXvtr].Enabled then Exit;
// QO-100 / split-LO: TX идёт на собственный uplink-LO (visible - TxLOOffset),
// без LOError — дрейф LNB к передаче отношения не имеет.
if FXvtrSettings.Entries[FCurrentXvtr].FullDuplexTx then
Result := VisibleHz - FXvtrSettings.Entries[FCurrentXvtr].TxLOOffset
else
// Обычный трансвертер: RX=TX LO (с калибровкой LOError) — как раньше.
Result := VisibleHz
- FXvtrSettings.Entries[FCurrentXvtr].LOOffset
+ FXvtrSettings.Entries[FCurrentXvtr].LOError;
end;
function TRadioController.ActiveTXFreqHz: Double;
begin
// TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B),
@@ -2238,6 +2258,8 @@ begin
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
end;
FDriveLevel := CalcDriveByte;
// QO-100 / split-LO: авто display-duplex (мониторинг своего downlink при TX).
if E.FullDuplexTx and not FDisplayDuplex then SetDuplex(True);
PushNetworkState;
Changed(rfXvtr);
@@ -2342,7 +2364,15 @@ begin
end;
procedure TRadioController.SetDuplex(On_: Boolean);
begin FDisplayDuplex := On_; Changed(rfDuplex); { TODO wiring } end;
begin
FDisplayDuplex := On_;
// Применяем сразу: при передаче держим RX-IQ в DSP-цепи (full-duplex монитор —
// для QO-100 слышим свой downlink через спутник). SetMOX тоже это выставляет,
// но здесь — чтобы тумблер DUP действовал и в момент передачи.
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.KeepRXDuringTX := FDisplayDuplex;
Changed(rfDuplex);
end;
procedure TRadioController.SetWfAGC(On_: Boolean);
begin FWfAGCEnabled := On_; Changed(rfWfAGC); end;
@@ -2498,7 +2528,7 @@ begin
if FTransmitting and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.ClearDUCIQQueue;
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz),
FDriveLevel, True, True, True);
FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT
FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле
@@ -2548,7 +2578,7 @@ begin
if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz),
FDriveLevel, False, True, True);
FNetwork.ClearDUCIQQueue;
FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX)
@@ -2588,7 +2618,7 @@ begin
FDSPEngine.SetDriveLevel(FDrivePercent / 100.0);
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz),
FDriveLevel, FTransmitting, True, True);
FNetwork.SendFullHP;
end;
@@ -2739,7 +2769,7 @@ begin
// Сначала отправляем Run=1 — эмулятор/железо создают ddc_specific_thread
// (порт 1025) и rx_thread (порт 1035+) только после получения HP Run=1.
// DDC Specific и DUC Specific отправляем ПОСЛЕ, иначе порты ещё не открыты.
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, False, True, True);
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FDriveLevel, False, True, True);
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel);
FRunning := True;
FRXStartTime := GetTickCount64;
@@ -2777,7 +2807,7 @@ procedure TRadioController.StopRunning;
begin
if FRunning then
begin
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), 0, False, True, True);
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), 0, False, True, True);
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
FRunning := False;
FTransmitting := False;