feat(slices): мультислайс-TX — выбор TX-слайса, кросс-банд (Flex-паритет)

- RadioController: FTxSliceId (0=главный VFO, >0=слайс), SetTxSlice
  эксклюзивно; ActiveTXFreqHz/ActiveTXMode с TX-слайса, режим применяется
  в SetMOX(True) перед SetTXRun; TxBandIdx для PA-кал и safety DoNotTx
  при кросс-банде; RemoveSlice сбрасывает TX-слайс.
- HPSDRNetwork: TX-антенна выбиралась по RX-бэнду → по TX-частоте
  (TxAnt[FreqToBandIndex(TXFreqHz)]).
- MainForm/PanafallPanel: клик TX на флаге слайса → SetTxSlice, TX-бейджи
  (янтарь=выбран, красный=передаёт), RefreshTxIndicators по rfTransmitting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 18:15:52 +03:00
co-authored by Claude Opus 4.8
parent 9bb86b59b8
commit 8d65f4d6e8
5 changed files with 86 additions and 14 deletions
+4 -2
View File
@@ -1390,9 +1390,11 @@ begin
// Если активен XVTR с RX antenna override (XvtrRxAnt 1..3) — он переопределяет // Если активен XVTR с RX antenna override (XvtrRxAnt 1..3) — он переопределяет
// RX антенну. TX антенна для XVTR всё равно берётся из Alex.TxAnt[band] — // RX антенну. TX антенна для XVTR всё равно берётся из Alex.TxAnt[band] —
// обычно та же что для основного HF-band за этой IF-частотой. // обычно та же что для основного HF-band за этой IF-частотой.
BandIdx := FreqToBandIndex(RXFreqHz); BandIdx := FreqToBandIndex(RXFreqHz); // RX-банд (антенна RX + вторичный вход)
// TX-антенна — по TX-частоте (кросс-банд мультислайс-TX: TX-слайс может быть
// в другом диапазоне, чем RX). RX-антенна — по RX-частоте.
if Transmitting then if Transmitting then
AntVal := Alex.TxAnt[BandIdx] AntVal := Alex.TxAnt[FreqToBandIndex(TXFreqHz)]
else if (XvtrRxAnt >= 1) and (XvtrRxAnt <= 3) then else if (XvtrRxAnt >= 1) and (XvtrRxAnt <= 3) then
AntVal := XvtrRxAnt AntVal := XvtrRxAnt
else else
+14 -3
View File
@@ -573,6 +573,7 @@ type
procedure OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string); procedure OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
procedure OnVfoOverlaySplit(Sender: TObject); procedure OnVfoOverlaySplit(Sender: TObject);
procedure OnVfoOverlayTxSelect(Sender: TObject); procedure OnVfoOverlayTxSelect(Sender: TObject);
procedure RefreshTxIndicators; // TX-бейджи всех флагов
procedure OnVfoOverlaySliceSelect(Sender: TObject); // выбрать слайс активным procedure OnVfoOverlaySliceSelect(Sender: TObject); // выбрать слайс активным
procedure OnVfoOverlayClose(Sender: TObject); // × — удалить слайс procedure OnVfoOverlayClose(Sender: TObject); // × — удалить слайс
procedure OnVfoOverlayInvalidate(Sender: TObject); procedure OnVfoOverlayInvalidate(Sender: TObject);
@@ -1351,6 +1352,7 @@ begin
FSpecView.TXMode := FController.FTransmitting and not FController.FDisplayDuplex; FSpecView.TXMode := FController.FTransmitting and not FController.FDisplayDuplex;
FSpecView.TXOverlay := FController.FTransmitting; FSpecView.TXOverlay := FController.FTransmitting;
ApplySpecViewGridFromState; // сетка спектра под активный тракт ApplySpecViewGridFromState; // сетка спектра под активный тракт
RefreshTxIndicators; // TX-бейджи флагов (какой слайс передаёт)
end; // (DUC-очередь сбрасывает SetMOX) end; // (DUC-очередь сбрасывает SetMOX)
rfTuning: rfTuning:
StyleButton(BtnTUN, FController.FTuning); StyleButton(BtnTUN, FController.FTuning);
@@ -5385,10 +5387,19 @@ end;
procedure TMainForm.OnVfoOverlayTxSelect(Sender: TObject); procedure TMainForm.OnVfoOverlayTxSelect(Sender: TObject);
begin begin
// TODO(слайсы): выбор TX-слайса при полноценной мультислайс-передаче. // Мультислайс-TX: клик по TX-бейджу делает слайс источником передачи
// Пока передаёт только главный канал — клик по TX неглавного игнорируем. // (эксклюзивно). CurSliceId=0 — главный VFO.
if CurSliceId <> 0 then Exit; FController.SetTxSlice(CurSliceId); // → Changed(rfTransmitting) → RefreshTxIndicators
end;
procedure TMainForm.RefreshTxIndicators;
// Обновить TX-бейджи (TxSel/Tx) на всех флагах всех панов после смены
// TX-слайса / состояния передачи.
var i: Integer;
begin
FPan.PushMainFlagExtState; FPan.PushMainFlagExtState;
for i := 0 to MAX_PANS - 1 do
if FPans[i] <> nil then FPans[i].PushAllSliceFlagStates;
end; end;
procedure TMainForm.OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string); procedure TMainForm.OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
+8 -3
View File
@@ -1315,7 +1315,10 @@ begin
O.SetState(S.Mode, BW, S.TargetHz, FController.SliceSMeter(SliceId)); O.SetState(S.Mode, BW, S.TargetHz, FController.SliceSMeter(SliceId));
O.SetDSPState(0, 0, False, False, FController.FAGCMode); O.SetDSPState(0, 0, False, False, FController.FAGCMode);
O.SetSquelchState(S.FMSQOn, S.FMSQLevel); O.SetSquelchState(S.FMSQOn, S.FMSQLevel);
O.SetExtState(Round(S.Volume * 100), S.Muted, False, False, False); // TxSel: слайс — источник передачи; Tx: передача идёт именно с него.
O.SetExtState(Round(S.Volume * 100), S.Muted, False,
FController.FTransmitting and (FController.FTxSliceId = SliceId),
FController.FTxSliceId = SliceId);
// Список аудио-устройств (общий PA-список) + текущее устройство слайса. // Список аудио-устройств (общий PA-список) + текущее устройство слайса.
if Assigned(FController.FAudioOut) then if Assigned(FController.FAudioOut) then
begin begin
@@ -1342,9 +1345,11 @@ end;
procedure TPanafallPanel.PushMainFlagExtState; procedure TPanafallPanel.PushMainFlagExtState;
begin begin
if not (Assigned(FMainFlag) and FMainFlag.Visible) or (FController = nil) then Exit; if not (Assigned(FMainFlag) and FMainFlag.Visible) or (FController = nil) then Exit;
// TxSel: этот слайс выбран для передачи. Пока TX только главный → всегда True. // Главный VFO = «слайс A» (id 0). TxSel/Tx активны, когда TX-источник = главный.
FMainFlag.SetExtState(FController.ActiveVolume, FController.FMuted, FMainFlag.SetExtState(FController.ActiveVolume, FController.FMuted,
FController.FSplitTxB, FController.FTransmitting, True); FController.FSplitTxB,
FController.FTransmitting and (FController.FTxSliceId = 0),
FController.FTxSliceId = 0);
end; end;
procedure TPanafallPanel.PushMainFlagAudioDevices; procedure TPanafallPanel.PushMainFlagAudioDevices;
+57 -4
View File
@@ -263,6 +263,7 @@ type
FTransmitting: Boolean; FTransmitting: Boolean;
FTuning: Boolean; FTuning: Boolean;
FSplitTxB: Boolean; FSplitTxB: Boolean;
FTxSliceId: Integer; // 0 = главный VFO (A/B split); >0 = слайс-источник TX
FVfoLock: Boolean; // блокировка перестройки VFO (Andromeda Lock) FVfoLock: Boolean; // блокировка перестройки VFO (Andromeda Lock)
FDisplayDuplex: Boolean; FDisplayDuplex: Boolean;
// QO-100 full-duplex self-monitor: True — RX-аудио глушится на TX (по умолч.), // QO-100 full-duplex self-monitor: True — RX-аудио глушится на TX (по умолч.),
@@ -427,6 +428,13 @@ type
// uplink-LO не подвержен дрейфу LNB). // uplink-LO не подвержен дрейфу LNB).
function XvtrTranslateTX(VisibleHz: Double): Double; function XvtrTranslateTX(VisibleHz: Double): Double;
function ActiveTXFreqHz: Double; function ActiveTXFreqHz: Double;
// Режим передающего тракта: слайс-источник TX (FTxSliceId) либо главный FMode.
function ActiveTXMode: Integer;
// Диапазон для калибровки драйва/антенны TX (банд TX-слайса при кросс-банде).
function TxBandIdx: Integer;
// Выбор слайса-источника передачи (0 = главный VFO). Эксклюзивно.
procedure SetTxSlice(SliceId: Integer);
property TxSliceId: Integer read FTxSliceId;
// Складывает коррекцию маяка dLock в LOError активного трансвертера (живая // Складывает коррекцию маяка dLock в LOError активного трансвертера (живая
// калибровка LNB), ретюнит радио и троттлингом сохраняет на диск. // калибровка LNB), ретюнит радио и троттлингом сохраняет на диск.
procedure ApplyBeaconLOError(dLock: Double); procedure ApplyBeaconLOError(dLock: Double);
@@ -1489,6 +1497,8 @@ var idx: Integer;
begin begin
idx := FindSliceIndex(Id); idx := FindSliceIndex(Id);
if idx < 0 then Exit; if idx < 0 then Exit;
// Удаляемый слайс был источником TX — вернуть передачу на главный VFO.
if FTxSliceId = Id then SetTxSlice(0);
if Assigned(FDSPEngine) then FDSPEngine.RemoveSlice(Id); if Assigned(FDSPEngine) then FDSPEngine.RemoveSlice(Id);
if FSlices[idx].Audio <> nil then if FSlices[idx].Audio <> nil then
begin begin
@@ -1757,7 +1767,12 @@ begin
end; end;
function TRadioController.ActiveTXFreqHz: Double; function TRadioController.ActiveTXFreqHz: Double;
var S: TCtrlSlice;
begin begin
// Мультислайс-TX: если выбран слайс-источник — передаём на его частоте
// (слайсы не несут repeater-конфиг, FM-сдвиг не применяем).
if (FTxSliceId > 0) and GetSlice(FTxSliceId, S) then
Exit(S.TargetHz);
// TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B), // TX-VFO задаётся ТОЛЬКО селектором FSplitTxB (UI: кнопки TX-A/TX-B),
// независимо от активного RX-VFO. Раньше при не-split TX ошибочно следовал // независимо от активного RX-VFO. Раньше при не-split TX ошибочно следовал
// за активным RX (TX-A горит, но передача шла на B при активном B). // за активным RX (TX-A горит, но передача шла на B при активном B).
@@ -1770,6 +1785,41 @@ begin
end; end;
end; end;
function TRadioController.ActiveTXMode: Integer;
var S: TCtrlSlice;
begin
if (FTxSliceId > 0) and GetSlice(FTxSliceId, S) then Result := S.Mode
else Result := FMode;
end;
function TRadioController.TxBandIdx: Integer;
begin
// При TX на слайсе другого диапазона (только КВ) калибровка/антенна — по
// банду TX-частоты, иначе главный банд.
Result := FCurrentBand;
if (FTxSliceId > 0) and (FCurrentXvtr < 0) then
begin
Result := FreqToBandIdx(ActiveTXFreqHz, IsPluto);
if (Result < 0) or (Result >= CFG_BAND_COUNT) then Result := FCurrentBand;
end;
end;
procedure TRadioController.SetTxSlice(SliceId: Integer);
var S: TCtrlSlice;
begin
if SliceId < 0 then SliceId := 0;
// Слайс-источник должен существовать (0 = главный всегда валиден).
if (SliceId > 0) and not GetSlice(SliceId, S) then Exit;
if FTxSliceId = SliceId then Exit;
FTxSliceId := SliceId;
// Передающий тракт под режим нового источника (idle-канал, применится к PTT).
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetTXMode(ActiveTXMode);
FDriveLevel := CalcDriveByte; // калибровка драйва банда TX-слайса
PushNetworkState; // TX-частота/Alex через ActiveTXFreqHz/TxBandIdx
Changed(rfTransmitting); // UI: перерисовать TX-бейджи флагов/оверлей
end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// QO-100 beacon lock // QO-100 beacon lock
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -2118,8 +2168,8 @@ begin
FXvtrSettings.Entries[FCurrentXvtr].Enabled; FXvtrSettings.Entries[FCurrentXvtr].Enabled;
if XvtrActive then if XvtrActive then
Cal := FVHFBandCal[FCurrentXvtr] Cal := FVHFBandCal[FCurrentXvtr]
else if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then else if (TxBandIdx >= 0) and (TxBandIdx < CFG_BAND_COUNT) then
Cal := FPABandCal[FCurrentBand] Cal := FPABandCal[TxBandIdx] // банд TX-слайса при кросс-банд TX
else else
Cal := 38.8; Cal := 38.8;
@@ -3581,7 +3631,7 @@ var
begin begin
// Safety: блокируем TX на бэндах с DoNotTx (Alex) и на RX-only трансвертере. // Safety: блокируем TX на бэндах с DoNotTx (Alex) и на RX-only трансвертере.
if On_ and FDevConnected and (FCurrentXvtr < 0) if On_ and FDevConnected and (FCurrentXvtr < 0)
and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then and FAlexSettings.DoNotTx[EnsureRange(TxBandIdx, 0, 10)] then
Exit; Exit;
if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then
@@ -3640,6 +3690,9 @@ begin
// В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы // В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы
// TX leakage не накапливался в RXA pipeline / FFT-истории анализатора. // TX leakage не накапливался в RXA pipeline / FFT-истории анализатора.
FDSPEngine.KeepRXDuringTX := FDisplayDuplex; FDSPEngine.KeepRXDuringTX := FDisplayDuplex;
// Мультислайс-TX: передающий тракт под режим слайса-источника (SetMode мог
// оставить главный режим). Ставим перед стартом TX-потока.
if FTransmitting then FDSPEngine.SetTXMode(ActiveTXMode);
FDSPEngine.SetTXRun(FTransmitting); FDSPEngine.SetTXRun(FTransmitting);
if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then if WasTransmitting and (not FTransmitting) and (not FDisplayDuplex) then
begin begin
@@ -3677,7 +3730,7 @@ begin
// Safety: те же блокировки, что и в SetMOX — TUN это TX через WDSP PostGen-тон, // Safety: те же блокировки, что и в SetMOX — TUN это TX через WDSP PostGen-тон,
// он тоже должен молчать на бэндах с DoNotTx и на RX-only XVTR. // он тоже должен молчать на бэндах с DoNotTx и на RX-only XVTR.
if On_ and FDevConnected and (FCurrentXvtr < 0) if On_ and FDevConnected and (FCurrentXvtr < 0)
and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then Exit; and FAlexSettings.DoNotTx[EnsureRange(TxBandIdx, 0, 10)] then Exit;
if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) if On_ and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT)
and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then Exit; and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then Exit;
if On_ then if On_ then
+3 -2
View File
@@ -17,9 +17,9 @@
<UseVersionInfo Value="True"/> <UseVersionInfo Value="True"/>
<AutoIncrementBuild Value="True"/> <AutoIncrementBuild Value="True"/>
<MinorVersionNr Value="9"/> <MinorVersionNr Value="9"/>
<BuildNr Value="257"/> <BuildNr Value="258"/>
</VersionInfo> </VersionInfo>
<MacroValues Count="76"> <MacroValues Count="77">
<Macro1 Name="LCLWidgetType" Value="qt6"/> <Macro1 Name="LCLWidgetType" Value="qt6"/>
<Macro2 Name="LCLWidgetType" Value="qt6"/> <Macro2 Name="LCLWidgetType" Value="qt6"/>
<Macro3 Name="LCLWidgetType" Value="qt6"/> <Macro3 Name="LCLWidgetType" Value="qt6"/>
@@ -96,6 +96,7 @@
<Macro74 Name="LCLWidgetType" Value="qt6"/> <Macro74 Name="LCLWidgetType" Value="qt6"/>
<Macro75 Name="LCLWidgetType" Value="qt6"/> <Macro75 Name="LCLWidgetType" Value="qt6"/>
<Macro76 Name="LCLWidgetType" Value="qt6"/> <Macro76 Name="LCLWidgetType" Value="qt6"/>
<Macro77 Name="LCLWidgetType" Value="qt6"/>
</MacroValues> </MacroValues>
<BuildModes> <BuildModes>
<Item Name="Debug" Default="True"/> <Item Name="Debug" Default="True"/>