diff --git a/MainForm.pas b/MainForm.pas index a9f266b..b4e2523 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -478,6 +478,7 @@ type // XVTR helpers function FindXvtrIdxForFreq(FreqHz: Double): Integer; function XvtrTranslate(VisibleHz: Double): Double; + function XvtrTranslateTX(VisibleHz: Double): Double; procedure ApplyXvtrToNetwork; procedure RebuildXvtrButtons; procedure RelayoutBelowBands; @@ -3777,7 +3778,7 @@ begin if FController.FNetwork.Connected and FController.FNetwork.Running then begin - FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, + FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; @@ -3799,7 +3800,7 @@ begin if FController.FNetwork.Connected and FController.FNetwork.Running then begin - FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, + FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; @@ -4115,7 +4116,7 @@ begin if FController.FNetwork.Connected and FController.FNetwork.Running then begin FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), - XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); + XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; if FController.FCurrentXvtr < 0 then @@ -5073,7 +5074,7 @@ begin end; FController.FDriveLevel := CalcDriveByte; if FController.FNetwork.Connected and FController.FNetwork.Running then - FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); + FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); end; procedure TMainForm.OnPlutoTxMaxAttChange(MaxAttDb: Double); @@ -5095,7 +5096,7 @@ begin end; FController.FDriveLevel := CalcDriveByte; if FController.FNetwork.Connected and FController.FNetwork.Running then - FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); + FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); end; procedure TMainForm.OnTXSettingsChange(const T: TTXSettings); @@ -5137,7 +5138,7 @@ begin FController.FDSPEngine.SetDriveLevel(FController.FTXSettings.TUNLevel / 100.0); if FController.FNetwork.Connected and FController.FNetwork.Running then begin - FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FController.FDriveLevel, + FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), FController.FDriveLevel, FController.FTransmitting, True, True); FController.FNetwork.SendFullHP; end; @@ -5187,6 +5188,12 @@ begin Result := FController.XvtrTranslate(VisibleHz); end; +function TMainForm.XvtrTranslateTX(VisibleHz: Double): Double; +begin + // TX-трансляция (QO-100 split-LO); обёртка над контроллером. + Result := FController.XvtrTranslateTX(VisibleHz); +end; + // Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его // настроек. Управляет XVTR Enable bit, T/R relay suppression. // XVTR DDC In (Alex bits 8+11) управляется через галочку XVTR в Antenna/Alex diff --git a/RadioController.pas b/RadioController.pas index 3a0d00c..6fa1e75 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -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; diff --git a/Settings.pas b/Settings.pas index 60665eb..6db72e2 100644 --- a/Settings.pas +++ b/Settings.pas @@ -12,6 +12,9 @@ const SETTINGS_FILE = 'hpsdr_settings.json'; CFG_BAND_COUNT = 11; CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов) + QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100 + // (split-LO транспондер; настраивается на + // отдельной странице, не в общей XVTR-таблице) // PA gain defaults (dB) — match Thetis HERMES/HPSDR defaults // order: 160m,80m,60m,40m,30m,20m,17m,15m,12m,10m,6m CFG_PA_BAND_CAL_DEFAULT: array[0..CFG_BAND_COUNT-1] of Double = ( @@ -155,8 +158,13 @@ type ButtonText: string[15]; // подпись на band-кнопке (например, '2m', '70cm') FreqBegin: Double; // Hz FreqEnd: Double; // Hz - LOOffset: Double; // Hz, отнимается от видимой частоты для получения IF - LOError: Double; // Hz, корректировка LO (положительная — IF растёт) + LOOffset: Double; // Hz, отнимается от видимой частоты для получения IF (RX) + LOError: Double; // Hz, корректировка LO (положительная — IF растёт) — RX/LNB дрейф + // QO-100 / split-LO транспондер: RX и TX идут на РАЗНЫХ LO. RX = visible-LOOffset + // (+LOError, дрейф LNB по маяку). TX = visible-TxLOOffset (без LOError — uplink-LO + // собственный у SDR). FullDuplexTx помечает слот как такой транспондер. + FullDuplexTx: Boolean; // True — раздельный TX-LO (QO-100); авто display-duplex + TxLOOffset: Double; // Hz, TX LO-offset (для QO-100 = транспондер-offset, ~8089.5 МГц) RXGain: Double; // dB, компенсация усиления RX тракта RXOnly: Boolean; // True — запрещает TX на этом трансверторе TXPower: Integer; // 0..100 % (drive level специально для XVTR) @@ -297,7 +305,8 @@ type procedure JW(O: TJSONObject; const K: string; V: Boolean); overload; procedure JWS(O: TJSONObject; const K: string; const V: string); procedure MigrateXvtrTemplate(var X: TXvtrSettings; - const AName: string; ABegin, AEnd, ALO: Double); + const AName: string; ABegin, AEnd, ALO: Double; + AFullDup: Boolean = False; ATxLO: Double = 0.0); public constructor Create(const FilePath: string = ''); destructor Destroy; override; @@ -1218,6 +1227,8 @@ begin X.Entries[i].FreqEnd := 0.0; X.Entries[i].LOOffset := 0.0; X.Entries[i].LOError := 0.0; + X.Entries[i].FullDuplexTx := False; + X.Entries[i].TxLOOffset := 0.0; X.Entries[i].RXGain := 0.0; X.Entries[i].RXOnly := False; X.Entries[i].TXPower := 100; @@ -1257,12 +1268,15 @@ begin X.Entries[1].LastFreqB := 433000000; // QO-100 (Es'hail-2) NB downlink: 10489.500–10490.000 МГц, LO 9750 МГц. // RX-трансвертер: tune = downlink − LO. Калибровать LOError по маяку. - X.Entries[2].ButtonText := 'QO-100'; - X.Entries[2].FreqBegin := 10489500000; - X.Entries[2].FreqEnd := 10490000000; - X.Entries[2].LOOffset := 9750000000; - X.Entries[2].LastFreq := 10489750000; - X.Entries[2].LastFreqB := 10489750000; + // QO-100 — в зарезервированном последнем слоте (отдельная страница настроек). + X.Entries[QO100_SLOT].ButtonText := 'QO-100'; + X.Entries[QO100_SLOT].FreqBegin := 10489500000; + X.Entries[QO100_SLOT].FreqEnd := 10490000000; + X.Entries[QO100_SLOT].LOOffset := 9750000000; // LNB LO + X.Entries[QO100_SLOT].FullDuplexTx := True; // раздельный TX-LO + full-duplex + X.Entries[QO100_SLOT].TxLOOffset := 8089500000; // транспондер-offset (downlink-uplink) + X.Entries[QO100_SLOT].LastFreq := 10489750000; + X.Entries[QO100_SLOT].LastFreqB := 10489750000; end; function TSettingsManager.LoadXvtr(const MAC: array of Byte; @@ -1290,6 +1304,8 @@ begin X.Entries[i].FreqEnd := JD(EObj, 'fend', X.Entries[i].FreqEnd); X.Entries[i].LOOffset := JD(EObj, 'lo_off', X.Entries[i].LOOffset); X.Entries[i].LOError := JD(EObj, 'lo_err', X.Entries[i].LOError); + X.Entries[i].FullDuplexTx := JB(EObj, 'full_dup', X.Entries[i].FullDuplexTx); + X.Entries[i].TxLOOffset := JD(EObj, 'tx_lo_off', X.Entries[i].TxLOOffset); X.Entries[i].RXGain := JD(EObj, 'rx_gain', X.Entries[i].RXGain); X.Entries[i].RXOnly := JB(EObj, 'rx_only', X.Entries[i].RXOnly); X.Entries[i].TXPower := EnsureRange(JI(EObj, 'tx_pwr', X.Entries[i].TXPower), 0, 100); @@ -1321,14 +1337,30 @@ begin (X.Entries[i].LastFreqB > X.Entries[i].FreqEnd) then X.Entries[i].LastFreqB := (X.Entries[i].FreqBegin + X.Entries[i].FreqEnd) / 2.0; end; - // Миграция: на устройствах с уже сохранённым xvtr-конфигом шаблон QO-100 из - // DefaultXvtr был перекрыт загруженными слотами. Если слота 'QO-100' нет — - // вписать шаблон (disabled) в первый свободный слот, чтобы он был в таблице. - MigrateXvtrTemplate(X, 'QO-100', 10489500000, 10490000000, 9750000000); + // QO-100 закреплён за зарезервированным слотом QO100_SLOT. Восстанавливаем + // шаблон, если слот не размечен как split-LO транспондер ИЛИ значения пустые + // (старый конфиг сохранял слот как обычный пустой XVTR с нулями — тогда + // FullDuplexTx по умолчанию True, но LO/offset == 0). Реальный QO-100 всегда + // имеет положительный транспондер-offset, так что валидный конфиг не трогаем. + if (not X.Entries[QO100_SLOT].FullDuplexTx) + or (X.Entries[QO100_SLOT].TxLOOffset <= 0) + or (X.Entries[QO100_SLOT].LOOffset <= 0) then + begin + X.Entries[QO100_SLOT].Enabled := False; + X.Entries[QO100_SLOT].ButtonText := 'QO-100'; + X.Entries[QO100_SLOT].FreqBegin := 10489500000; + X.Entries[QO100_SLOT].FreqEnd := 10490000000; + X.Entries[QO100_SLOT].LOOffset := 9750000000; + X.Entries[QO100_SLOT].FullDuplexTx := True; + X.Entries[QO100_SLOT].TxLOOffset := 8089500000; + X.Entries[QO100_SLOT].LastFreq := 10489750000; + X.Entries[QO100_SLOT].LastFreqB := 10489750000; + end; end; procedure TSettingsManager.MigrateXvtrTemplate(var X: TXvtrSettings; - const AName: string; ABegin, AEnd, ALO: Double); + const AName: string; ABegin, AEnd, ALO: Double; + AFullDup: Boolean = False; ATxLO: Double = 0.0); var i, freeIdx: Integer; begin @@ -1341,12 +1373,14 @@ begin freeIdx := i; end; if freeIdx < 0 then Exit; // нет свободных слотов - X.Entries[freeIdx].ButtonText := AName; - X.Entries[freeIdx].FreqBegin := ABegin; - X.Entries[freeIdx].FreqEnd := AEnd; - X.Entries[freeIdx].LOOffset := ALO; - X.Entries[freeIdx].LastFreq := (ABegin + AEnd) / 2.0; - X.Entries[freeIdx].LastFreqB := (ABegin + AEnd) / 2.0; + X.Entries[freeIdx].ButtonText := AName; + X.Entries[freeIdx].FreqBegin := ABegin; + X.Entries[freeIdx].FreqEnd := AEnd; + X.Entries[freeIdx].LOOffset := ALO; + X.Entries[freeIdx].FullDuplexTx := AFullDup; + X.Entries[freeIdx].TxLOOffset := ATxLO; + X.Entries[freeIdx].LastFreq := (ABegin + AEnd) / 2.0; + X.Entries[freeIdx].LastFreqB := (ABegin + AEnd) / 2.0; end; procedure TSettingsManager.SaveXvtr(const MAC: array of Byte; @@ -1366,6 +1400,8 @@ begin JW(EObj, 'fend', X.Entries[i].FreqEnd); JW(EObj, 'lo_off', X.Entries[i].LOOffset); JW(EObj, 'lo_err', X.Entries[i].LOError); + JW(EObj, 'full_dup', X.Entries[i].FullDuplexTx); + JW(EObj, 'tx_lo_off', X.Entries[i].TxLOOffset); JW(EObj, 'rx_gain', X.Entries[i].RXGain); JW(EObj, 'rx_only', X.Entries[i].RXOnly); JW(EObj, 'tx_pwr', X.Entries[i].TXPower); diff --git a/SettingsForm.pas b/SettingsForm.pas index 516ef05..19f7a1f 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -243,6 +243,18 @@ type FXvtrConfig: TXvtrSettings; FOnXvtrChange: TOnXvtrChange; + // ---- QO-100 tab (редактирует зарезервированный слот QO100_SLOT) ---- + FPageQO: TScrollBox; + FNavQO: TFlatButton; + FQoEn: TFlatCheckBox; + FQoLnbLO: TFlatFloatSpinEdit; // МГц — LNB LO (= LOOffset, RX) + FQoTxLO: TFlatFloatSpinEdit; // МГц — транспондер-offset (= TxLOOffset) + FQoBegin: TFlatFloatSpinEdit; // МГц — downlink begin + FQoEnd: TFlatFloatSpinEdit; // МГц — downlink end + FQoRXGain: TFlatFloatSpinEdit; // dB + FQoPower: TFlatSpinEdit; // 0..100 TX drive ceiling + FQoInfo: TLabel; // расчёт RX IF / TX freq + // ---- CAT tab controls ---- FCATSerialEn: array[0..3] of TFlatCheckBox; FCATSerialPort: array[0..3] of TFlatEdit; @@ -304,6 +316,11 @@ type procedure OnAlexAnyChange(Sender: TObject); procedure OnAlexRxOnlyChange(Sender: TObject); procedure BuildXvtrTab; + procedure BuildQOTab; + procedure CollectQOFromUI; // QO-виджеты → FXvtrConfig[QO100_SLOT] + procedure LoadQOFromConfig; // FXvtrConfig[QO100_SLOT] → QO-виджеты + procedure UpdateQOInfo; // пересчёт RX IF / TX freq в инфо-метке + procedure OnQOAnyChange(Sender: TObject); procedure CollectXvtrFromUI; procedure FireXvtrChange; procedure OnXvtrAnyChange(Sender: TObject); @@ -636,6 +653,7 @@ begin FNavCAT := MakeNavButton('CAT', 326); FNavAlex := MakeNavButton('Antenna / Alex', 362); FNavXvtr := MakeNavButton('Transverter', 398); + FNavQO := MakeNavButton('QO-100', 434); FContentPanel := TPanel.Create(Self); FContentPanel.Parent := Self; @@ -653,6 +671,7 @@ begin FPageCAT := MakeScrollPage; FPageAlex := MakeScrollPage; FPageXvtr := MakeScrollPage; + FPageQO := MakeScrollPage; FAlexBoardType := 0; TSettingsManager.DefaultAlex(FAlexConfig); TSettingsManager.DefaultXvtr(FXvtrConfig); @@ -666,6 +685,7 @@ begin BuildCATTab; BuildAlexTab; BuildXvtrTab; + BuildQOTab; FBtnClose := TFlatButton.Create(Self); FBtnClose.Parent := Self; @@ -696,7 +716,8 @@ begin else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced) else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT) else if Sender = FNavAlex then SelectPage(FPageAlex, FNavAlex) - else if Sender = FNavXvtr then SelectPage(FPageXvtr, FNavXvtr); + else if Sender = FNavXvtr then SelectPage(FPageXvtr, FNavXvtr) + else if Sender = FNavQO then SelectPage(FPageQO, FNavQO); end; procedure TSettingsForm.SelectPage(APage: TWinControl; ANav: TFlatButton); @@ -711,6 +732,7 @@ begin FPageCAT.Visible := APage = FPageCAT; FPageAlex.Visible := APage = FPageAlex; FPageXvtr.Visible := APage = FPageXvtr; + FPageQO.Visible := APage = FPageQO; FNavAudio.Active := ANav = FNavAudio; FNavDisplay.Active := ANav = FNavDisplay; @@ -722,6 +744,7 @@ begin FNavCAT.Active := ANav = FNavCAT; FNavAlex.Active := ANav = FNavAlex; FNavXvtr.Active := ANav = FNavXvtr; + FNavQO.Active := ANav = FNavQO; end; // --------------------------------------------------------------------------- @@ -3228,10 +3251,10 @@ begin L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM; L.WordWrap := True; - // --- Group "Slots" --- + // --- Group "Slots" --- (последний слот зарезервирован под QO-100 — не строим) GrpY := 100; Grp := MakeGroupPanel(FPageXvtr, 'Slots', - MARGIN, GrpY, GRP_W, GRP_TITLE_H + HDR_H + CFG_XVTR_COUNT * ROW_H + 14); + MARGIN, GrpY, GRP_W, GRP_TITLE_H + HDR_H + QO100_SLOT * ROW_H + 14); // Заголовки колонок (под GRP_TITLE_H + небольшой отступ) Y := GRP_TITLE_H + 6; @@ -3247,8 +3270,8 @@ begin MakeHdr(Grp, 'Disable' + LineEnding + 'PA', PA_X, Y, PA_W); MakeHdr(Grp, 'RX' + LineEnding + 'Antenna', ANT_X, Y, ANT_W); - // Ряды слотов - for i := 0 to CFG_XVTR_COUNT - 1 do + // Ряды слотов (QO100_SLOT исключён — у него отдельная страница QO-100) + for i := 0 to QO100_SLOT - 1 do begin Y := GRP_TITLE_H + HDR_H + i * ROW_H + 2; // Чекбокс EN центрируем по 24px ширине EN-колонки @@ -3276,7 +3299,7 @@ begin end; // --- Group "Options" --- - GrpY := GrpY + GRP_TITLE_H + HDR_H + CFG_XVTR_COUNT * ROW_H + 14 + 18; + GrpY := GrpY + GRP_TITLE_H + HDR_H + QO100_SLOT * ROW_H + 14 + 18; GrpOpts := MakeGroupPanel(FPageXvtr, 'Options', MARGIN, GrpY, GRP_W, GRP_TITLE_H + 36); FChkXvtrTunPwr := TFlatCheckBox.Create(Self); @@ -3355,7 +3378,8 @@ procedure TSettingsForm.FireXvtrChange; var i: Integer; begin if FLoading then Exit; - CollectXvtrFromUI; + CollectXvtrFromUI; // слоты 0..QO100_SLOT-1 (виджеты QO-слота не созданы → skip) + CollectQOFromUI; // QO100_SLOT — из QO-страницы // Перерисовать enabled-состояние строк (если изменилась галка En) for i := 0 to CFG_XVTR_COUNT - 1 do UpdateXvtrRowEnabled(i); @@ -3400,9 +3424,154 @@ begin end; end; if FChkXvtrTunPwr <> nil then FChkXvtrTunPwr.Checked := X.UseXVTRTunePower; + LoadQOFromConfig; // QO100_SLOT → отдельная страница QO-100 finally FLoading := False; end; end; +// ── QO-100 page ───────────────────────────────────────────────────────────── + +procedure TSettingsForm.BuildQOTab; +const + MARGIN = 22; + LBL_X = 14; LBL_W = 230; + ED_X = 250; ED_W = 130; +var + Grp: TPanel; + L: TLabel; + Y: Integer; + + function QoFloat(AY: Integer; AMin, AMax, AStep: Double; ADec: Integer): TFlatFloatSpinEdit; + begin + Result := TFlatFloatSpinEdit.Create(Self); + Result.Parent := Grp; + Result.SetBounds(ED_X, AY, ED_W, BTN_H); + Result.MinValue := AMin; Result.MaxValue := AMax; + Result.Increment := AStep; Result.DecimalPlaces := ADec; + Result.Color := CLR_INPUT; Result.Font.Color := CLR_INPUT_TEXT; + Result.Font.Name := UI_FONT; Result.Font.Size := 9; + Result.OnChange := OnQOAnyChange; + end; + + procedure QoLabel(const Cap: string; AY: Integer); + begin + L := TLabel.Create(Self); + L.Parent := Grp; L.Caption := Cap; + L.SetBounds(LBL_X, AY + 4, LBL_W, 18); + L.Font.Color := CLR_TEXTDIM; L.Font.Name := UI_FONT; L.Font.Size := 9; + end; + +begin + FLoading := True; + try + L := TLabel.Create(Self); + L.Parent := FPageQO; L.Caption := 'QO-100 (Es''hail-2 NB transponder)'; + L.SetBounds(MARGIN, 18, 600, 28); + L.Font.Name := UI_FONT; L.Font.Size := 16; + L.Font.Style := [fsBold]; L.Font.Color := CLR_TEXT; + + L := TLabel.Create(Self); + L.Parent := FPageQO; + L.Caption := 'Geostationary transponder: RX downlink (~10489.5 MHz) via LNB, ' + + 'TX uplink (~2400 MHz) direct. VFO is displayed in downlink terms; ' + + 'RX and TX use separate LOs. Drift is corrected by the beacon lock.'; + L.SetBounds(MARGIN, 50, 700, 34); + L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM; + L.WordWrap := True; + + Grp := MakeGroupPanel(FPageQO, 'Transponder', MARGIN, 100, 560, GRP_TITLE_H + 7 * 30 + 16); + + Y := GRP_TITLE_H + 8; + FQoEn := TFlatCheckBox.Create(Self); + FQoEn.Parent := Grp; FQoEn.Caption := 'Enable QO-100 band'; + FQoEn.SetBounds(LBL_X, Y, 300, 20); + FQoEn.Font.Color := CLR_TEXT; FQoEn.Font.Name := UI_FONT; FQoEn.Font.Size := 9; + FQoEn.OnChange := OnQOAnyChange; + + Inc(Y, 32); QoLabel('LNB LO (MHz)', Y); + FQoLnbLO := QoFloat(Y, 0.0, 50000.0, 0.001, 6); + FQoLnbLO.Value := 9750.0; + + Inc(Y, 30); QoLabel('Transponder offset = downlink - uplink (MHz)', Y); + FQoTxLO := QoFloat(Y, 0.0, 50000.0, 0.001, 6); + FQoTxLO.Value := 8089.5; + + Inc(Y, 30); QoLabel('Downlink begin (MHz)', Y); + FQoBegin := QoFloat(Y, 0.0, 50000.0, 0.001, 3); + FQoBegin.Value := 10489.5; + + Inc(Y, 30); QoLabel('Downlink end (MHz)', Y); + FQoEnd := QoFloat(Y, 0.0, 50000.0, 0.001, 3); + FQoEnd.Value := 10490.0; + + Inc(Y, 30); QoLabel('RX gain offset (dB)', Y); + FQoRXGain := QoFloat(Y, -60.0, 60.0, 0.1, 1); + + Inc(Y, 30); QoLabel('TX drive ceiling (%)', Y); + FQoPower := TFlatSpinEdit.Create(Self); + FQoPower.Parent := Grp; FQoPower.SetBounds(ED_X, Y, ED_W, BTN_H); + FQoPower.MinValue := 0; FQoPower.MaxValue := 100; FQoPower.Value := 100; + FQoPower.Color := CLR_INPUT; FQoPower.Font.Color := CLR_INPUT_TEXT; + FQoPower.Font.Name := UI_FONT; FQoPower.Font.Size := 9; + FQoPower.OnChange := OnQOAnyChange; + + FQoInfo := TLabel.Create(Self); + FQoInfo.Parent := FPageQO; + FQoInfo.SetBounds(MARGIN, 100 + GRP_TITLE_H + 7 * 30 + 28, 700, 52); + FQoInfo.Font.Color := CLR_TEXTDIM; FQoInfo.Font.Name := 'Courier New'; + FQoInfo.Font.Size := 9; FQoInfo.WordWrap := True; + finally + FLoading := False; + end; +end; + +procedure TSettingsForm.UpdateQOInfo; +var dlMid, rxIf, txF: Double; +begin + if FQoInfo = nil then Exit; + dlMid := (FQoBegin.Value + FQoEnd.Value) / 2.0; // МГц + rxIf := dlMid - FQoLnbLO.Value; // RX IF в SDR + txF := dlMid - FQoTxLO.Value; // TX uplink + FQoInfo.Caption := Format( + 'At downlink %.3f MHz: RX IF = %.3f MHz TX = %.3f MHz', + [dlMid, rxIf, txF]); +end; + +procedure TSettingsForm.CollectQOFromUI; +begin + if FQoEn = nil then Exit; + FXvtrConfig.Entries[QO100_SLOT].Enabled := FQoEn.Checked; + FXvtrConfig.Entries[QO100_SLOT].FullDuplexTx := True; // слот всегда QO-100 + FXvtrConfig.Entries[QO100_SLOT].ButtonText := 'QO-100'; + FXvtrConfig.Entries[QO100_SLOT].LOOffset := FQoLnbLO.Value * 1.0e6; + FXvtrConfig.Entries[QO100_SLOT].TxLOOffset := FQoTxLO.Value * 1.0e6; + FXvtrConfig.Entries[QO100_SLOT].FreqBegin := FQoBegin.Value * 1.0e6; + FXvtrConfig.Entries[QO100_SLOT].FreqEnd := FQoEnd.Value * 1.0e6; + FXvtrConfig.Entries[QO100_SLOT].RXGain := FQoRXGain.Value; + FXvtrConfig.Entries[QO100_SLOT].TXPower := FQoPower.Value; +end; + +procedure TSettingsForm.LoadQOFromConfig; +var E: TXvtrEntry; +begin + if FQoEn = nil then Exit; + E := FXvtrConfig.Entries[QO100_SLOT]; + FQoEn.Checked := E.Enabled; + FQoLnbLO.Value := E.LOOffset / 1.0e6; + FQoTxLO.Value := E.TxLOOffset / 1.0e6; + FQoBegin.Value := E.FreqBegin / 1.0e6; + FQoEnd.Value := E.FreqEnd / 1.0e6; + FQoRXGain.Value := E.RXGain; + FQoPower.Value := EnsureRange(E.TXPower, 0, 100); + UpdateQOInfo; +end; + +procedure TSettingsForm.OnQOAnyChange(Sender: TObject); +begin + if FLoading then Exit; + UpdateQOInfo; + FireXvtrChange; +end; + end. diff --git a/doc/PLUTO_INTEGRATION_PLAN.md b/doc/PLUTO_INTEGRATION_PLAN.md index c976418..045eb15 100644 --- a/doc/PLUTO_INTEGRATION_PLAN.md +++ b/doc/PLUTO_INTEGRATION_PLAN.md @@ -291,14 +291,17 @@ end; - TODO: проверка на железе (ориентация SSB по Q-инверсии, уровни); полифазный ресэмпл (см. §4.6); опц. зануление IQ на приёме. -**Фаза 5 — Режим QO-100** +**Фаза 5 — Режим QO-100** ✅ - ✅ Калибровка LNB по маяку (beacon lock) — стабилизация дрейфа. -- ✅ Hardware full-duplex: TX-стрим живёт всё время Run (Фаза 4), RX+TX - одновременно. Остаётся display-duplex (`SetDuplex` — водопад RX во время TX). -- TODO: `TQO100Settings` + раздельные RX/TX LO (§3.2), дисплей в downlink-терминах, - довести `SetDuplex`. -- UI: вход/выход в режим QO-100 (как XVTR-слот), поля LNB LO / ppm / TX atten. -- ✅ полноценная работа через спутник. +- ✅ Hardware full-duplex + ✅ display-duplex (`SetDuplex` доведён: живой + `KeepRXDuringTX`; авто-вкл при входе в QO-100 — слышим свой downlink). +- ✅ Раздельные RX/TX LO (§3.2): QO-100 = XVTR-слот с флагом `FullDuplexTx` + + `TxLOOffset`; `XvtrTranslateTX` даёт TX-LO = visible−TxLOOffset (без LOError). + RX-путь (LNB LO + beacon-дрейф) не тронут. Дисплей/VFO в downlink-терминах. +- ✅ Отдельная страница «QO-100» в SettingsForm (LNB LO / транспондер-offset / + границы downlink / RX gain / TX-потолок); зарезервирован последний XVTR-слот + (`QO100_SLOT`), не засоряет таблицу трансвертеров openHPSDR. +- TODO: проверка на железе. **Фаза 6 — Полировка** - Web/CAT-фронтенды (команды уже агностичны) + гейтинг полей; демон `ewsdrd.lpr`