mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
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:
+13
-6
@@ -478,6 +478,7 @@ type
|
|||||||
// XVTR helpers
|
// XVTR helpers
|
||||||
function FindXvtrIdxForFreq(FreqHz: Double): Integer;
|
function FindXvtrIdxForFreq(FreqHz: Double): Integer;
|
||||||
function XvtrTranslate(VisibleHz: Double): Double;
|
function XvtrTranslate(VisibleHz: Double): Double;
|
||||||
|
function XvtrTranslateTX(VisibleHz: Double): Double;
|
||||||
procedure ApplyXvtrToNetwork;
|
procedure ApplyXvtrToNetwork;
|
||||||
procedure RebuildXvtrButtons;
|
procedure RebuildXvtrButtons;
|
||||||
procedure RelayoutBelowBands;
|
procedure RelayoutBelowBands;
|
||||||
@@ -3777,7 +3778,7 @@ begin
|
|||||||
|
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
begin
|
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.FTransmitting, True, True);
|
||||||
FController.FNetwork.SendFullHP;
|
FController.FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -3799,7 +3800,7 @@ begin
|
|||||||
|
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
begin
|
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.FTransmitting, True, True);
|
||||||
FController.FNetwork.SendFullHP;
|
FController.FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -4115,7 +4116,7 @@ begin
|
|||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq),
|
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;
|
FController.FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
if FController.FCurrentXvtr < 0 then
|
if FController.FCurrentXvtr < 0 then
|
||||||
@@ -5073,7 +5074,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
FController.FDriveLevel := CalcDriveByte;
|
FController.FDriveLevel := CalcDriveByte;
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
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;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnPlutoTxMaxAttChange(MaxAttDb: Double);
|
procedure TMainForm.OnPlutoTxMaxAttChange(MaxAttDb: Double);
|
||||||
@@ -5095,7 +5096,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
FController.FDriveLevel := CalcDriveByte;
|
FController.FDriveLevel := CalcDriveByte;
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
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;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnTXSettingsChange(const T: TTXSettings);
|
procedure TMainForm.OnTXSettingsChange(const T: TTXSettings);
|
||||||
@@ -5137,7 +5138,7 @@ begin
|
|||||||
FController.FDSPEngine.SetDriveLevel(FController.FTXSettings.TUNLevel / 100.0);
|
FController.FDSPEngine.SetDriveLevel(FController.FTXSettings.TUNLevel / 100.0);
|
||||||
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
if FController.FNetwork.Connected and FController.FNetwork.Running then
|
||||||
begin
|
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.FTransmitting, True, True);
|
||||||
FController.FNetwork.SendFullHP;
|
FController.FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -5187,6 +5188,12 @@ begin
|
|||||||
Result := FController.XvtrTranslate(VisibleHz);
|
Result := FController.XvtrTranslate(VisibleHz);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TMainForm.XvtrTranslateTX(VisibleHz: Double): Double;
|
||||||
|
begin
|
||||||
|
// TX-трансляция (QO-100 split-LO); обёртка над контроллером.
|
||||||
|
Result := FController.XvtrTranslateTX(VisibleHz);
|
||||||
|
end;
|
||||||
|
|
||||||
// Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его
|
// Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его
|
||||||
// настроек. Управляет XVTR Enable bit, T/R relay suppression.
|
// настроек. Управляет XVTR Enable bit, T/R relay suppression.
|
||||||
// XVTR DDC In (Alex bits 8+11) управляется через галочку XVTR в Antenna/Alex
|
// XVTR DDC In (Alex bits 8+11) управляется через галочку XVTR в Antenna/Alex
|
||||||
|
|||||||
+37
-7
@@ -347,6 +347,10 @@ type
|
|||||||
// активного трансвертора (LOOffset/LOError).
|
// активного трансвертора (LOOffset/LOError).
|
||||||
// ActiveTXFreqHz — текущая TX-частота (split/active VFO + FM-репитер сдвиг).
|
// ActiveTXFreqHz — текущая TX-частота (split/active VFO + FM-репитер сдвиг).
|
||||||
function XvtrTranslate(VisibleHz: Double): Double;
|
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;
|
function ActiveTXFreqHz: Double;
|
||||||
// Измерение маяка по display-FFT в окне ±WinHz вокруг ExpectOff (baseband):
|
// Измерение маяка по display-FFT в окне ±WinHz вокруг ExpectOff (baseband):
|
||||||
// центроид лепестка → уточнённое смещение, «высота» пика над средним окна
|
// центроид лепестка → уточнённое смещение, «высота» пика над средним окна
|
||||||
@@ -629,7 +633,7 @@ procedure TRadioController.PushNetworkState;
|
|||||||
begin
|
begin
|
||||||
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
|
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz),
|
||||||
FDriveLevel, FTransmitting, True, True);
|
FDriveLevel, FTransmitting, True, True);
|
||||||
FNetwork.SendFullHP;
|
FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -979,6 +983,22 @@ begin
|
|||||||
+ FXvtrSettings.Entries[FCurrentXvtr].LOError;
|
+ FXvtrSettings.Entries[FCurrentXvtr].LOError;
|
||||||
end;
|
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;
|
function TRadioController.ActiveTXFreqHz: Double;
|
||||||
begin
|
begin
|
||||||
// TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B),
|
// TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B),
|
||||||
@@ -2238,6 +2258,8 @@ begin
|
|||||||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
|
||||||
end;
|
end;
|
||||||
FDriveLevel := CalcDriveByte;
|
FDriveLevel := CalcDriveByte;
|
||||||
|
// QO-100 / split-LO: авто display-duplex (мониторинг своего downlink при TX).
|
||||||
|
if E.FullDuplexTx and not FDisplayDuplex then SetDuplex(True);
|
||||||
PushNetworkState;
|
PushNetworkState;
|
||||||
|
|
||||||
Changed(rfXvtr);
|
Changed(rfXvtr);
|
||||||
@@ -2342,7 +2364,15 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.SetDuplex(On_: Boolean);
|
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);
|
procedure TRadioController.SetWfAGC(On_: Boolean);
|
||||||
begin FWfAGCEnabled := On_; Changed(rfWfAGC); end;
|
begin FWfAGCEnabled := On_; Changed(rfWfAGC); end;
|
||||||
@@ -2498,7 +2528,7 @@ begin
|
|||||||
if FTransmitting and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
if FTransmitting and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FNetwork.ClearDUCIQQueue;
|
FNetwork.ClearDUCIQQueue;
|
||||||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
|
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz),
|
||||||
FDriveLevel, True, True, True);
|
FDriveLevel, True, True, True);
|
||||||
FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT
|
FNetwork.SendFullHP; // 1й пакет: Alex bit27=1 (T/R relay), drive — без PTT
|
||||||
FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле
|
FNetwork.SendPTT(True); // 2й пакет: PTT=1 после переключения реле
|
||||||
@@ -2548,7 +2578,7 @@ begin
|
|||||||
if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT
|
FNetwork.SendPTT(False); // 1й пакет: PTT=0 — сначала снимаем PTT
|
||||||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
|
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz),
|
||||||
FDriveLevel, False, True, True);
|
FDriveLevel, False, True, True);
|
||||||
FNetwork.ClearDUCIQQueue;
|
FNetwork.ClearDUCIQQueue;
|
||||||
FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX)
|
FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX)
|
||||||
@@ -2588,7 +2618,7 @@ begin
|
|||||||
FDSPEngine.SetDriveLevel(FDrivePercent / 100.0);
|
FDSPEngine.SetDriveLevel(FDrivePercent / 100.0);
|
||||||
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
if Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz),
|
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz),
|
||||||
FDriveLevel, FTransmitting, True, True);
|
FDriveLevel, FTransmitting, True, True);
|
||||||
FNetwork.SendFullHP;
|
FNetwork.SendFullHP;
|
||||||
end;
|
end;
|
||||||
@@ -2739,7 +2769,7 @@ begin
|
|||||||
// Сначала отправляем Run=1 — эмулятор/железо создают ddc_specific_thread
|
// Сначала отправляем Run=1 — эмулятор/железо создают ddc_specific_thread
|
||||||
// (порт 1025) и rx_thread (порт 1035+) только после получения HP Run=1.
|
// (порт 1025) и rx_thread (порт 1035+) только после получения HP Run=1.
|
||||||
// DDC Specific и DUC Specific отправляем ПОСЛЕ, иначе порты ещё не открыты.
|
// 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);
|
FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel);
|
||||||
FRunning := True;
|
FRunning := True;
|
||||||
FRXStartTime := GetTickCount64;
|
FRXStartTime := GetTickCount64;
|
||||||
@@ -2777,7 +2807,7 @@ procedure TRadioController.StopRunning;
|
|||||||
begin
|
begin
|
||||||
if FRunning then
|
if FRunning then
|
||||||
begin
|
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);
|
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
|
||||||
FRunning := False;
|
FRunning := False;
|
||||||
FTransmitting := False;
|
FTransmitting := False;
|
||||||
|
|||||||
+50
-14
@@ -12,6 +12,9 @@ const
|
|||||||
SETTINGS_FILE = 'hpsdr_settings.json';
|
SETTINGS_FILE = 'hpsdr_settings.json';
|
||||||
CFG_BAND_COUNT = 11;
|
CFG_BAND_COUNT = 11;
|
||||||
CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов)
|
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
|
// PA gain defaults (dB) — match Thetis HERMES/HPSDR defaults
|
||||||
// order: 160m,80m,60m,40m,30m,20m,17m,15m,12m,10m,6m
|
// 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 = (
|
CFG_PA_BAND_CAL_DEFAULT: array[0..CFG_BAND_COUNT-1] of Double = (
|
||||||
@@ -155,8 +158,13 @@ type
|
|||||||
ButtonText: string[15]; // подпись на band-кнопке (например, '2m', '70cm')
|
ButtonText: string[15]; // подпись на band-кнопке (например, '2m', '70cm')
|
||||||
FreqBegin: Double; // Hz
|
FreqBegin: Double; // Hz
|
||||||
FreqEnd: Double; // Hz
|
FreqEnd: Double; // Hz
|
||||||
LOOffset: Double; // Hz, отнимается от видимой частоты для получения IF
|
LOOffset: Double; // Hz, отнимается от видимой частоты для получения IF (RX)
|
||||||
LOError: Double; // Hz, корректировка LO (положительная — IF растёт)
|
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 тракта
|
RXGain: Double; // dB, компенсация усиления RX тракта
|
||||||
RXOnly: Boolean; // True — запрещает TX на этом трансверторе
|
RXOnly: Boolean; // True — запрещает TX на этом трансверторе
|
||||||
TXPower: Integer; // 0..100 % (drive level специально для XVTR)
|
TXPower: Integer; // 0..100 % (drive level специально для XVTR)
|
||||||
@@ -297,7 +305,8 @@ type
|
|||||||
procedure JW(O: TJSONObject; const K: string; V: Boolean); overload;
|
procedure JW(O: TJSONObject; const K: string; V: Boolean); overload;
|
||||||
procedure JWS(O: TJSONObject; const K: string; const V: string);
|
procedure JWS(O: TJSONObject; const K: string; const V: string);
|
||||||
procedure MigrateXvtrTemplate(var X: TXvtrSettings;
|
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
|
public
|
||||||
constructor Create(const FilePath: string = '');
|
constructor Create(const FilePath: string = '');
|
||||||
destructor Destroy; override;
|
destructor Destroy; override;
|
||||||
@@ -1218,6 +1227,8 @@ begin
|
|||||||
X.Entries[i].FreqEnd := 0.0;
|
X.Entries[i].FreqEnd := 0.0;
|
||||||
X.Entries[i].LOOffset := 0.0;
|
X.Entries[i].LOOffset := 0.0;
|
||||||
X.Entries[i].LOError := 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].RXGain := 0.0;
|
||||||
X.Entries[i].RXOnly := False;
|
X.Entries[i].RXOnly := False;
|
||||||
X.Entries[i].TXPower := 100;
|
X.Entries[i].TXPower := 100;
|
||||||
@@ -1257,12 +1268,15 @@ begin
|
|||||||
X.Entries[1].LastFreqB := 433000000;
|
X.Entries[1].LastFreqB := 433000000;
|
||||||
// QO-100 (Es'hail-2) NB downlink: 10489.500–10490.000 МГц, LO 9750 МГц.
|
// QO-100 (Es'hail-2) NB downlink: 10489.500–10490.000 МГц, LO 9750 МГц.
|
||||||
// RX-трансвертер: tune = downlink − LO. Калибровать LOError по маяку.
|
// RX-трансвертер: tune = downlink − LO. Калибровать LOError по маяку.
|
||||||
X.Entries[2].ButtonText := 'QO-100';
|
// QO-100 — в зарезервированном последнем слоте (отдельная страница настроек).
|
||||||
X.Entries[2].FreqBegin := 10489500000;
|
X.Entries[QO100_SLOT].ButtonText := 'QO-100';
|
||||||
X.Entries[2].FreqEnd := 10490000000;
|
X.Entries[QO100_SLOT].FreqBegin := 10489500000;
|
||||||
X.Entries[2].LOOffset := 9750000000;
|
X.Entries[QO100_SLOT].FreqEnd := 10490000000;
|
||||||
X.Entries[2].LastFreq := 10489750000;
|
X.Entries[QO100_SLOT].LOOffset := 9750000000; // LNB LO
|
||||||
X.Entries[2].LastFreqB := 10489750000;
|
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;
|
end;
|
||||||
|
|
||||||
function TSettingsManager.LoadXvtr(const MAC: array of Byte;
|
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].FreqEnd := JD(EObj, 'fend', X.Entries[i].FreqEnd);
|
||||||
X.Entries[i].LOOffset := JD(EObj, 'lo_off', X.Entries[i].LOOffset);
|
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].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].RXGain := JD(EObj, 'rx_gain', X.Entries[i].RXGain);
|
||||||
X.Entries[i].RXOnly := JB(EObj, 'rx_only', X.Entries[i].RXOnly);
|
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);
|
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].FreqEnd) then
|
||||||
X.Entries[i].LastFreqB := (X.Entries[i].FreqBegin + X.Entries[i].FreqEnd) / 2.0;
|
X.Entries[i].LastFreqB := (X.Entries[i].FreqBegin + X.Entries[i].FreqEnd) / 2.0;
|
||||||
end;
|
end;
|
||||||
// Миграция: на устройствах с уже сохранённым xvtr-конфигом шаблон QO-100 из
|
// QO-100 закреплён за зарезервированным слотом QO100_SLOT. Восстанавливаем
|
||||||
// DefaultXvtr был перекрыт загруженными слотами. Если слота 'QO-100' нет —
|
// шаблон, если слот не размечен как split-LO транспондер ИЛИ значения пустые
|
||||||
// вписать шаблон (disabled) в первый свободный слот, чтобы он был в таблице.
|
// (старый конфиг сохранял слот как обычный пустой XVTR с нулями — тогда
|
||||||
MigrateXvtrTemplate(X, 'QO-100', 10489500000, 10490000000, 9750000000);
|
// 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;
|
end;
|
||||||
|
|
||||||
procedure TSettingsManager.MigrateXvtrTemplate(var X: TXvtrSettings;
|
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
|
var
|
||||||
i, freeIdx: Integer;
|
i, freeIdx: Integer;
|
||||||
begin
|
begin
|
||||||
@@ -1345,6 +1377,8 @@ begin
|
|||||||
X.Entries[freeIdx].FreqBegin := ABegin;
|
X.Entries[freeIdx].FreqBegin := ABegin;
|
||||||
X.Entries[freeIdx].FreqEnd := AEnd;
|
X.Entries[freeIdx].FreqEnd := AEnd;
|
||||||
X.Entries[freeIdx].LOOffset := ALO;
|
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].LastFreq := (ABegin + AEnd) / 2.0;
|
||||||
X.Entries[freeIdx].LastFreqB := (ABegin + AEnd) / 2.0;
|
X.Entries[freeIdx].LastFreqB := (ABegin + AEnd) / 2.0;
|
||||||
end;
|
end;
|
||||||
@@ -1366,6 +1400,8 @@ begin
|
|||||||
JW(EObj, 'fend', X.Entries[i].FreqEnd);
|
JW(EObj, 'fend', X.Entries[i].FreqEnd);
|
||||||
JW(EObj, 'lo_off', X.Entries[i].LOOffset);
|
JW(EObj, 'lo_off', X.Entries[i].LOOffset);
|
||||||
JW(EObj, 'lo_err', X.Entries[i].LOError);
|
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_gain', X.Entries[i].RXGain);
|
||||||
JW(EObj, 'rx_only', X.Entries[i].RXOnly);
|
JW(EObj, 'rx_only', X.Entries[i].RXOnly);
|
||||||
JW(EObj, 'tx_pwr', X.Entries[i].TXPower);
|
JW(EObj, 'tx_pwr', X.Entries[i].TXPower);
|
||||||
|
|||||||
+176
-7
@@ -243,6 +243,18 @@ type
|
|||||||
FXvtrConfig: TXvtrSettings;
|
FXvtrConfig: TXvtrSettings;
|
||||||
FOnXvtrChange: TOnXvtrChange;
|
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 ----
|
// ---- CAT tab controls ----
|
||||||
FCATSerialEn: array[0..3] of TFlatCheckBox;
|
FCATSerialEn: array[0..3] of TFlatCheckBox;
|
||||||
FCATSerialPort: array[0..3] of TFlatEdit;
|
FCATSerialPort: array[0..3] of TFlatEdit;
|
||||||
@@ -304,6 +316,11 @@ type
|
|||||||
procedure OnAlexAnyChange(Sender: TObject);
|
procedure OnAlexAnyChange(Sender: TObject);
|
||||||
procedure OnAlexRxOnlyChange(Sender: TObject);
|
procedure OnAlexRxOnlyChange(Sender: TObject);
|
||||||
procedure BuildXvtrTab;
|
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 CollectXvtrFromUI;
|
||||||
procedure FireXvtrChange;
|
procedure FireXvtrChange;
|
||||||
procedure OnXvtrAnyChange(Sender: TObject);
|
procedure OnXvtrAnyChange(Sender: TObject);
|
||||||
@@ -636,6 +653,7 @@ begin
|
|||||||
FNavCAT := MakeNavButton('CAT', 326);
|
FNavCAT := MakeNavButton('CAT', 326);
|
||||||
FNavAlex := MakeNavButton('Antenna / Alex', 362);
|
FNavAlex := MakeNavButton('Antenna / Alex', 362);
|
||||||
FNavXvtr := MakeNavButton('Transverter', 398);
|
FNavXvtr := MakeNavButton('Transverter', 398);
|
||||||
|
FNavQO := MakeNavButton('QO-100', 434);
|
||||||
|
|
||||||
FContentPanel := TPanel.Create(Self);
|
FContentPanel := TPanel.Create(Self);
|
||||||
FContentPanel.Parent := Self;
|
FContentPanel.Parent := Self;
|
||||||
@@ -653,6 +671,7 @@ begin
|
|||||||
FPageCAT := MakeScrollPage;
|
FPageCAT := MakeScrollPage;
|
||||||
FPageAlex := MakeScrollPage;
|
FPageAlex := MakeScrollPage;
|
||||||
FPageXvtr := MakeScrollPage;
|
FPageXvtr := MakeScrollPage;
|
||||||
|
FPageQO := MakeScrollPage;
|
||||||
FAlexBoardType := 0;
|
FAlexBoardType := 0;
|
||||||
TSettingsManager.DefaultAlex(FAlexConfig);
|
TSettingsManager.DefaultAlex(FAlexConfig);
|
||||||
TSettingsManager.DefaultXvtr(FXvtrConfig);
|
TSettingsManager.DefaultXvtr(FXvtrConfig);
|
||||||
@@ -666,6 +685,7 @@ begin
|
|||||||
BuildCATTab;
|
BuildCATTab;
|
||||||
BuildAlexTab;
|
BuildAlexTab;
|
||||||
BuildXvtrTab;
|
BuildXvtrTab;
|
||||||
|
BuildQOTab;
|
||||||
|
|
||||||
FBtnClose := TFlatButton.Create(Self);
|
FBtnClose := TFlatButton.Create(Self);
|
||||||
FBtnClose.Parent := Self;
|
FBtnClose.Parent := Self;
|
||||||
@@ -696,7 +716,8 @@ begin
|
|||||||
else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced)
|
else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced)
|
||||||
else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT)
|
else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT)
|
||||||
else if Sender = FNavAlex then SelectPage(FPageAlex, FNavAlex)
|
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;
|
end;
|
||||||
|
|
||||||
procedure TSettingsForm.SelectPage(APage: TWinControl; ANav: TFlatButton);
|
procedure TSettingsForm.SelectPage(APage: TWinControl; ANav: TFlatButton);
|
||||||
@@ -711,6 +732,7 @@ begin
|
|||||||
FPageCAT.Visible := APage = FPageCAT;
|
FPageCAT.Visible := APage = FPageCAT;
|
||||||
FPageAlex.Visible := APage = FPageAlex;
|
FPageAlex.Visible := APage = FPageAlex;
|
||||||
FPageXvtr.Visible := APage = FPageXvtr;
|
FPageXvtr.Visible := APage = FPageXvtr;
|
||||||
|
FPageQO.Visible := APage = FPageQO;
|
||||||
|
|
||||||
FNavAudio.Active := ANav = FNavAudio;
|
FNavAudio.Active := ANav = FNavAudio;
|
||||||
FNavDisplay.Active := ANav = FNavDisplay;
|
FNavDisplay.Active := ANav = FNavDisplay;
|
||||||
@@ -722,6 +744,7 @@ begin
|
|||||||
FNavCAT.Active := ANav = FNavCAT;
|
FNavCAT.Active := ANav = FNavCAT;
|
||||||
FNavAlex.Active := ANav = FNavAlex;
|
FNavAlex.Active := ANav = FNavAlex;
|
||||||
FNavXvtr.Active := ANav = FNavXvtr;
|
FNavXvtr.Active := ANav = FNavXvtr;
|
||||||
|
FNavQO.Active := ANav = FNavQO;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -3228,10 +3251,10 @@ begin
|
|||||||
L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM;
|
L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM;
|
||||||
L.WordWrap := True;
|
L.WordWrap := True;
|
||||||
|
|
||||||
// --- Group "Slots" ---
|
// --- Group "Slots" --- (последний слот зарезервирован под QO-100 — не строим)
|
||||||
GrpY := 100;
|
GrpY := 100;
|
||||||
Grp := MakeGroupPanel(FPageXvtr, 'Slots',
|
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 + небольшой отступ)
|
// Заголовки колонок (под GRP_TITLE_H + небольшой отступ)
|
||||||
Y := GRP_TITLE_H + 6;
|
Y := GRP_TITLE_H + 6;
|
||||||
@@ -3247,8 +3270,8 @@ begin
|
|||||||
MakeHdr(Grp, 'Disable' + LineEnding + 'PA', PA_X, Y, PA_W);
|
MakeHdr(Grp, 'Disable' + LineEnding + 'PA', PA_X, Y, PA_W);
|
||||||
MakeHdr(Grp, 'RX' + LineEnding + 'Antenna', ANT_X, Y, ANT_W);
|
MakeHdr(Grp, 'RX' + LineEnding + 'Antenna', ANT_X, Y, ANT_W);
|
||||||
|
|
||||||
// Ряды слотов
|
// Ряды слотов (QO100_SLOT исключён — у него отдельная страница QO-100)
|
||||||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
for i := 0 to QO100_SLOT - 1 do
|
||||||
begin
|
begin
|
||||||
Y := GRP_TITLE_H + HDR_H + i * ROW_H + 2;
|
Y := GRP_TITLE_H + HDR_H + i * ROW_H + 2;
|
||||||
// Чекбокс EN центрируем по 24px ширине EN-колонки
|
// Чекбокс EN центрируем по 24px ширине EN-колонки
|
||||||
@@ -3276,7 +3299,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// --- Group "Options" ---
|
// --- 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',
|
GrpOpts := MakeGroupPanel(FPageXvtr, 'Options',
|
||||||
MARGIN, GrpY, GRP_W, GRP_TITLE_H + 36);
|
MARGIN, GrpY, GRP_W, GRP_TITLE_H + 36);
|
||||||
FChkXvtrTunPwr := TFlatCheckBox.Create(Self);
|
FChkXvtrTunPwr := TFlatCheckBox.Create(Self);
|
||||||
@@ -3355,7 +3378,8 @@ procedure TSettingsForm.FireXvtrChange;
|
|||||||
var i: Integer;
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
if FLoading then Exit;
|
if FLoading then Exit;
|
||||||
CollectXvtrFromUI;
|
CollectXvtrFromUI; // слоты 0..QO100_SLOT-1 (виджеты QO-слота не созданы → skip)
|
||||||
|
CollectQOFromUI; // QO100_SLOT — из QO-страницы
|
||||||
// Перерисовать enabled-состояние строк (если изменилась галка En)
|
// Перерисовать enabled-состояние строк (если изменилась галка En)
|
||||||
for i := 0 to CFG_XVTR_COUNT - 1 do
|
for i := 0 to CFG_XVTR_COUNT - 1 do
|
||||||
UpdateXvtrRowEnabled(i);
|
UpdateXvtrRowEnabled(i);
|
||||||
@@ -3400,9 +3424,154 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
if FChkXvtrTunPwr <> nil then FChkXvtrTunPwr.Checked := X.UseXVTRTunePower;
|
if FChkXvtrTunPwr <> nil then FChkXvtrTunPwr.Checked := X.UseXVTRTunePower;
|
||||||
|
LoadQOFromConfig; // QO100_SLOT → отдельная страница QO-100
|
||||||
finally
|
finally
|
||||||
FLoading := False;
|
FLoading := False;
|
||||||
end;
|
end;
|
||||||
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.
|
end.
|
||||||
|
|||||||
@@ -291,14 +291,17 @@ end;
|
|||||||
- TODO: проверка на железе (ориентация SSB по Q-инверсии, уровни); полифазный
|
- TODO: проверка на железе (ориентация SSB по Q-инверсии, уровни); полифазный
|
||||||
ресэмпл (см. §4.6); опц. зануление IQ на приёме.
|
ресэмпл (см. §4.6); опц. зануление IQ на приёме.
|
||||||
|
|
||||||
**Фаза 5 — Режим QO-100**
|
**Фаза 5 — Режим QO-100** ✅
|
||||||
- ✅ Калибровка LNB по маяку (beacon lock) — стабилизация дрейфа.
|
- ✅ Калибровка LNB по маяку (beacon lock) — стабилизация дрейфа.
|
||||||
- ✅ Hardware full-duplex: TX-стрим живёт всё время Run (Фаза 4), RX+TX
|
- ✅ Hardware full-duplex + ✅ display-duplex (`SetDuplex` доведён: живой
|
||||||
одновременно. Остаётся display-duplex (`SetDuplex` — водопад RX во время TX).
|
`KeepRXDuringTX`; авто-вкл при входе в QO-100 — слышим свой downlink).
|
||||||
- TODO: `TQO100Settings` + раздельные RX/TX LO (§3.2), дисплей в downlink-терминах,
|
- ✅ Раздельные RX/TX LO (§3.2): QO-100 = XVTR-слот с флагом `FullDuplexTx` +
|
||||||
довести `SetDuplex`.
|
`TxLOOffset`; `XvtrTranslateTX` даёт TX-LO = visible−TxLOOffset (без LOError).
|
||||||
- UI: вход/выход в режим QO-100 (как XVTR-слот), поля LNB LO / ppm / TX atten.
|
RX-путь (LNB LO + beacon-дрейф) не тронут. Дисплей/VFO в downlink-терминах.
|
||||||
- ✅ полноценная работа через спутник.
|
- ✅ Отдельная страница «QO-100» в SettingsForm (LNB LO / транспондер-offset /
|
||||||
|
границы downlink / RX gain / TX-потолок); зарезервирован последний XVTR-слот
|
||||||
|
(`QO100_SLOT`), не засоряет таблицу трансвертеров openHPSDR.
|
||||||
|
- TODO: проверка на железе.
|
||||||
|
|
||||||
**Фаза 6 — Полировка**
|
**Фаза 6 — Полировка**
|
||||||
- Web/CAT-фронтенды (команды уже агностичны) + гейтинг полей; демон `ewsdrd.lpr`
|
- Web/CAT-фронтенды (команды уже агностичны) + гейтинг полей; демон `ewsdrd.lpr`
|
||||||
|
|||||||
Reference in New Issue
Block a user