mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +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:
+56
-20
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user