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
+176 -7
View File
@@ -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.