diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas index c6042ec..cb2e938 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -693,6 +693,12 @@ begin // см. doc/SLICES_PLAN.md 3.1). Каждый DDC тюнится независимо. Result.MaxPans := 4; Result.IndependentPanFreq := True; + // Два АЦП у ANAN-100D/200D (ORION), 7000/8000 (ORION2) и Saturn/G2 — + // те же платы, что в RebuildDDCSpecific (Pkt.NumADCs). + if FDevice.BoardType in [4, 5, 10] then + Result.NumADCs := 2 + else + Result.NumADCs := 1; end; constructor THPSDRNetwork.Create; diff --git a/MainForm.pas b/MainForm.pas index 3b2c686..eba1cb5 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -187,6 +187,8 @@ type // Меню rate пана («NNN kHz» в шапке панов N; создаётся лениво). FPanRateMenu: TPopupMenu; FPanRateMenuPanId: Integer; + // Pop-out (3.5): пан N в отдельном OS-окне. nil = пан в стеке. + FPanFloatForm: array[1..MAX_PANS-1] of TForm; FSpecView: TSpectrumView; FWidebandView: TWidebandView; FUseOpenGLSpectrum: Boolean; @@ -575,6 +577,15 @@ type procedure UpdateAddPanButton; procedure OnPanRateClick(Sender: TObject); // «NNN kHz» шапки пана N procedure PanRateMenuItemClick(Sender: TObject); + procedure OnPanADCClick(Sender: TObject); // бейдж «A1/A2» (3.4) + // Pop-out (3.5): «⧉» выносит пан в отдельное окно, [×] окна возвращает в стек. + procedure OnPanPopOutClick(Sender: TObject); + procedure PopOutPan(PanId: Integer); + procedure DockBackPan(PanId: Integer); + procedure PanFloatClose(Sender: TObject; var CloseAction: TCloseAction); + procedure PanFloatResize(Sender: TObject); + function PanFloating(PanId: Integer): Boolean; + procedure ResetPanGLCaches(P: TPanafallPanel); // после смены родителя procedure AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double); procedure PanClickTune(P: TPanafallPanel; X, W: Integer); // клик по фону пана N procedure PanViewWindow(P: TPanafallPanel; out VC, VS: Double); @@ -1061,6 +1072,7 @@ begin // Стек панов (этап 3.3): пан 0 — в общем массиве; «⊞» добавляет пан. FPan.PanId := 0; FPan.OnAddPan := OnAddPanClick; + FPan.OnADCClick := OnPanADCClick; // бейдж A1/A2 (3.4, платы с 2 АЦП) FPans[0] := FPan; FPanShare[0] := 1.0; FPanCbLock := SyncObjs.TCriticalSection.Create; @@ -2566,9 +2578,11 @@ var N, i, k, TotalH, UsableH, Y, Hi: Integer; ShareSum: Double; begin + // Плавающие (pop-out) паны в стеке не участвуют — у них своё окно. N := 0; for i := 0 to MAX_PANS - 1 do - if FPans[i] <> nil then begin ActiveIds[N] := i; Inc(N); end; + if (FPans[i] <> nil) and not PanFloating(i) then + begin ActiveIds[N] := i; Inc(N); end; if N = 0 then Exit; TotalH := PanelRight.ClientHeight; UsableH := TotalH - (N - 1) * PAN_SPLIT_H; @@ -2679,6 +2693,7 @@ begin if FPans[i] <> nil then begin if FPans[i].BtnAddPan <> nil then StyleButton(FPans[i].BtnAddPan, False); + if FPans[i].BtnPopPan <> nil then StyleButton(FPans[i].BtnPopPan, False); if FPans[i].BtnClosePan <> nil then StyleButton(FPans[i].BtnClosePan, False); if FPans[i].HeaderPanel <> nil then begin @@ -2689,11 +2704,17 @@ begin FPans[i].HeaderRateLabel.Font.Color := T.BtnTextActive else FPans[i].HeaderRateLabel.Font.Color := T.Text; + // ADC-бейдж кликабелен на 2-АЦПшных платах — тоже акцент. + if FPans[i].ADCClickable then + FPans[i].HeaderADCLabel.Font.Color := T.BtnTextActive + else + FPans[i].HeaderADCLabel.Font.Color := T.Text; end; if i > 0 then begin FPans[i].SetFlagsTheme(T); if FPanSplitters[i] <> nil then FPanSplitters[i].Color := T.Border; + if FPanFloatForm[i] <> nil then FPanFloatForm[i].Color := T.BG; end; end; if FPanZoomBar <> nil then @@ -5355,6 +5376,120 @@ begin MarkPanDirty(P); end; +procedure TMainForm.OnPanADCClick(Sender: TObject); +// Бейдж «A1/A2»: тумблер ADC-источника пана (двух-АЦПшные платы, 3.4). +var + P: TPanafallPanel; + N: Integer; +begin + if not (Sender is TPanafallPanel) then Exit; + P := TPanafallPanel(Sender); + N := FController.BackendCaps.NumADCs; + if N < 2 then Exit; + if not FController.SetPanADC(P.PanId, (FController.PanADC(P.PanId) + 1) mod N) then + Exit; + UpdatePanHeaders; +end; + +// ---- Pop-out (3.5): пан N в отдельном OS-окне ---- + +function TMainForm.PanFloating(PanId: Integer): Boolean; +begin + Result := (PanId >= 1) and (PanId < MAX_PANS) and (FPanFloatForm[PanId] <> nil); +end; + +procedure TMainForm.ResetPanGLCaches(P: TPanafallPanel); +// Смена родителя пересоздаёт хэндлы GL-канв → старые контексты (и все +// текстуры) мертвы; кэши вьюхи обнуляем БЕЗ glDelete (как при смене MSAA). +begin + if FUseOpenGLSpectrum and (P.View is TSpectrumViewOpenGL) then + TSpectrumViewOpenGL(P.View).ResetGLCache; +end; + +procedure TMainForm.OnPanPopOutClick(Sender: TObject); +begin + if Sender is TPanafallPanel then + PopOutPan(TPanafallPanel(Sender).PanId); +end; + +procedure TMainForm.PopOutPan(PanId: Integer); +var + P: TPanafallPanel; + F: TForm; + T: TAppTheme; +begin + if (PanId < 1) or (PanId >= MAX_PANS) then Exit; + P := FPans[PanId]; + if (P = nil) or PanFloating(PanId) then Exit; + if FLightTheme then T := LightTheme else T := DarkTheme; + F := TForm.CreateNew(Self); + F.Caption := Format('EWSDR · PAN %d', [PanId]); + F.Color := T.BG; + F.Position := poDefault; + F.Width := PanelRight.ClientWidth; + F.Height := Max(300, P.StackHeight); + F.Tag := PanId; + F.OnClose := PanFloatClose; + F.OnResize := PanFloatResize; + F.OnMouseWheel := FormMouseWheel; // колесо над флагами/фоном как в стеке + FPanFloatForm[PanId] := F; + P.ReparentTo(F); + ResetPanGLCaches(P); + P.StackTop := 0; + P.StackHeight := 0; // 0/0 = весь родитель (окно) + if FPanSplitters[PanId] <> nil then FPanSplitters[PanId].Visible := False; + F.Show; + UpdatePanHeaders; + ResizeSpectrumPanels; // пере-стек оставшихся в главном окне + P.Layout(0, FController.FShowSpectrum, FController.FShowWaterfall); + P.LayoutFlags; + MarkPanDirty(P); +end; + +procedure TMainForm.DockBackPan(PanId: Integer); +var + P: TPanafallPanel; + F: TForm; +begin + P := FPans[PanId]; + F := FPanFloatForm[PanId]; + if F = nil then Exit; + FPanFloatForm[PanId] := nil; + if P <> nil then + begin + P.ReparentTo(PanelRight); + ResetPanGLCaches(P); + end; + if FPanSplitters[PanId] <> nil then FPanSplitters[PanId].Visible := True; + F.Release; // безопасно из событий самого окна + UpdatePanHeaders; + ResizeSpectrumPanels; + if P <> nil then + begin + P.LayoutFlags; + MarkPanDirty(P); + end; +end; + +procedure TMainForm.PanFloatClose(Sender: TObject; var CloseAction: TCloseAction); +// [×] окна = вернуть пан в стек (не закрыть пан). Release — в DockBackPan. +begin + CloseAction := caNone; + DockBackPan(TForm(Sender).Tag); +end; + +procedure TMainForm.PanFloatResize(Sender: TObject); +var P: TPanafallPanel; PanId: Integer; +begin + PanId := TForm(Sender).Tag; + if (PanId < 1) or (PanId >= MAX_PANS) then Exit; + P := FPans[PanId]; + if (P = nil) or (FPanFloatForm[PanId] <> Sender) then Exit; + P.Layout(0, FController.FShowSpectrum, FController.FShowWaterfall); + P.LayoutFlags; + MarkPanDirty(P); +end; + procedure TMainForm.PanClickTune(P: TPanafallPanel; X, W: Integer); // Клик по фону доп. пана = перестроить его слайс на частоту клика (аналог // DoSpectrumClick главного: снап к 100 Гц). Цель — активный слайс, если он @@ -5392,18 +5527,22 @@ begin end; procedure TMainForm.UpdatePanHeaders; -// Текст/видимость шапок: у одного пана шапки нет, у стека — у всех. +// Текст/видимость шапок: у одного пана шапки нет, у стека — у всех; +// у плавающего (pop-out) пана шапка всегда (там его rate/ADC/×). var - i, N: Integer; + i, N, NADC: Integer; P: TPanafallPanel; begin N := PanCount; + NADC := FController.BackendCaps.NumADCs; for i := 0 to MAX_PANS - 1 do begin P := FPans[i]; if P = nil then Continue; - P.HeaderVisible := N > 1; + P.HeaderVisible := (N > 1) or ((i > 0) and PanFloating(i)); if P.BtnClosePan <> nil then P.BtnClosePan.Visible := (N > 1) and (i > 0); + if P.BtnPopPan <> nil then + P.BtnPopPan.Visible := (N > 1) and (i > 0) and not PanFloating(i); if i = 0 then begin P.SetHeaderText(Format('PAN 0 · %.6f MHz ·', @@ -5416,6 +5555,17 @@ begin [i, FController.PanDDCFreq(i) / 1e6])); P.SetHeaderRate(Format('%d kHz', [FController.PanDDCRateKHz(i)])); end; + // Бейдж АЦП: только на двух-АЦПшных платах; клик = тумблер. + if NADC > 1 then + begin + P.SetHeaderADC(Format('A%d', [FController.PanADC(i) + 1])); + P.ADCClickable := True; + end + else + begin + P.SetHeaderADC(''); + P.ADCClickable := False; + end; end; end; @@ -5466,6 +5616,8 @@ begin P.OnSplitterMoved := RightPanelResize; // ratio обновлён → полный re-layout P.OnRateClick := OnPanRateClick; // «NNN kHz» в шапке → меню rate P.RateClickable := True; + P.OnADCClick := OnPanADCClick; // «A1/A2» тумблер АЦП (3.4) + P.OnPanPopOut := OnPanPopOutClick; // «⧉» в отдельное окно (3.5) P.SpectrumMouseDown := PanNSpectrumMouseDown; P.SpectrumMouseMove := PanNSpectrumMouseMove; P.SpectrumMouseUp := PanNSpectrumMouseUp; @@ -5555,6 +5707,13 @@ begin FActiveSliceId := 0; FreeAndNil(FPanSplitters[PanId]); P.Free; + // Пан был в отдельном окне — окно больше не нужно. Release (не Free): + // сюда можно попасть из событий контролов этого же окна («×» шапки → тик). + if FPanFloatForm[PanId] <> nil then + begin + FPanFloatForm[PanId].Release; + FPanFloatForm[PanId] := nil; + end; for i := 0 to MAX_PANS - 1 do if FPans[i] <> nil then FPanShare[i] := 1.0 / PanCount; UpdatePanHeaders; diff --git a/PanafallPanel.pas b/PanafallPanel.pas index 1690a28..8ed1893 100644 --- a/PanafallPanel.pas +++ b/PanafallPanel.pas @@ -98,6 +98,11 @@ type FHeaderRateLabel: TLabel; // «NNN kHz»; у панов N>0 кликабелен (селектор rate) FRateClickable: Boolean; FOnRateClick: TNotifyEvent; + FHeaderADCLabel: TLabel; // «A1»/«A2»; кликабелен при 2 АЦП (тумблер, 3.4) + FADCClickable: Boolean; + FOnADCClick: TNotifyEvent; + FBtnPopPan: TFlatButton; // «⧉» pop-out в отдельное окно (паны N>0, 3.5) + FOnPanPopOut: TNotifyEvent; FBtnClosePan: TFlatButton; // «×» в шапке (только паны N>0) FBtnAddPan: TFlatButton; // «⊞» в ряду пан/зума (только пан 0) FHeaderVisible: Boolean; @@ -110,6 +115,9 @@ type procedure BtnAddPanClick(Sender: TObject); procedure HeaderRateClick(Sender: TObject); procedure SetRateClickable(AValue: Boolean); + procedure HeaderADCClick(Sender: TObject); + procedure SetADCClickable(AValue: Boolean); + procedure BtnPopPanClick(Sender: TObject); procedure PositionHeaderChildren(RW, H: Integer); // Видимое частотное окно ЭТОГО пана: пан 0 — окно главного (зум/пан), // паны N — центр их DDC ± rate/2 (пан-зум — этап 3.3+). @@ -148,6 +156,10 @@ type // Создаёт контролы в AParent и подвешивает обработчики (см. поля выше). procedure Build(AParent: TWinControl; AMSAA: Integer); + // Переносит ВСЕ контролы панели в другого родителя (pop-out/dock, 3.5). + // GL-контексты канв при этом пересоздаются — хозяин обязан сбросить + // GL-кэши вьюхи (ResetGLCache) после вызова. + procedure ReparentTo(NewParent: TWinControl); // Полная раскладка стека. ATopOffset — высота wideband-блока хозяина // над спектром. Show-флаги приходят от контроллера (FShowSpectrum/ // FShowWaterfall). @@ -182,9 +194,18 @@ type procedure SetHeaderRate(const S: string); property RateClickable: Boolean read FRateClickable write SetRateClickable; property OnRateClick: TNotifyEvent read FOnRateClick write FOnRateClick; + // Бейдж ADC-источника «A1»/«A2» (3.4): показывается при NumADCs>1, + // клик = тумблер АЦП (хозяин). + procedure SetHeaderADC(const S: string); + property ADCClickable: Boolean read FADCClickable write SetADCClickable; + property OnADCClick: TNotifyEvent read FOnADCClick write FOnADCClick; + // «⧉» (3.5): вынести пан в отдельное окно (хозяин). + property OnPanPopOut: TNotifyEvent read FOnPanPopOut write FOnPanPopOut; property HeaderPanel: TPanel read FHeaderPanel; property HeaderLabel: TLabel read FHeaderLabel; property HeaderRateLabel: TLabel read FHeaderRateLabel; + property HeaderADCLabel: TLabel read FHeaderADCLabel; + property BtnPopPan: TFlatButton read FBtnPopPan; property BtnClosePan: TFlatButton read FBtnClosePan; property BtnAddPan: TFlatButton read FBtnAddPan; // Ряд пан/зума: у панов N>0 скрыт (их зум — этап 3.3+). @@ -380,10 +401,35 @@ begin FHeaderRateLabel.Parent := FHeaderPanel; FHeaderRateLabel.AutoSize := True; FHeaderRateLabel.OnClick := HeaderRateClick; + // ADC-бейдж «A1»/«A2» (3.4): пустой текст = скрыт (одноАЦПшные платы). + FHeaderADCLabel := TLabel.Create(Self); + FHeaderADCLabel.Parent := FHeaderPanel; + FHeaderADCLabel.AutoSize := True; + FHeaderADCLabel.OnClick := HeaderADCClick; + FBtnPopPan := MakeFlatBtn(FHeaderPanel, '⧉', 0, 0, 10, 10, BtnPopPanClick); + FBtnPopPan.Hint := 'Pop out'; + FBtnPopPan.ShowHint := True; + FBtnPopPan.Visible := False; // хозяин включает у панов N>0 FBtnClosePan := MakeFlatBtn(FHeaderPanel, '×', 0, 0, 10, 10, BtnClosePanClick); FBtnClosePan.Visible := False; // хозяин включает у панов N>0 end; +procedure TPanafallPanel.ReparentTo(NewParent: TWinControl); +begin + if (NewParent = nil) or (NewParent = FParent) then Exit; + FParent := NewParent; + FPbSpectrum.Parent := NewParent; + FPbRuler.Parent := NewParent; + FSplitter.Parent := NewParent; + FPbWaterfall.Parent := NewParent; + FPbPanZoom.Parent := NewParent; + FBtnZoomOut.Parent := NewParent; + FBtnZoomDef.Parent := NewParent; + FBtnZoomIn.Parent := NewParent; + FBtnAddPan.Parent := NewParent; + FHeaderPanel.Parent := NewParent; // дети шапки едут вместе с ней +end; + procedure TPanafallPanel.BtnClosePanClick(Sender: TObject); begin if Assigned(FOnPanClose) then FOnPanClose(Self); @@ -410,6 +456,8 @@ begin if FHeaderRateLabel.Caption <> S then FHeaderRateLabel.Caption := S; if FHeaderLabel <> nil then FHeaderRateLabel.Left := FHeaderLabel.Left + FHeaderLabel.Width + 6; + if FHeaderADCLabel <> nil then + FHeaderADCLabel.Left := FHeaderRateLabel.Left + FHeaderRateLabel.Width + 10; end; procedure TPanafallPanel.SetRateClickable(AValue: Boolean); @@ -429,6 +477,37 @@ begin if FRateClickable and Assigned(FOnRateClick) then FOnRateClick(Self); end; +procedure TPanafallPanel.SetHeaderADC(const S: string); +begin + if FHeaderADCLabel = nil then Exit; + if FHeaderADCLabel.Caption <> S then FHeaderADCLabel.Caption := S; + FHeaderADCLabel.Visible := S <> ''; + if FHeaderRateLabel <> nil then + FHeaderADCLabel.Left := FHeaderRateLabel.Left + FHeaderRateLabel.Width + 10; +end; + +procedure TPanafallPanel.SetADCClickable(AValue: Boolean); +begin + FADCClickable := AValue; + if FHeaderADCLabel <> nil then + begin + if AValue then + FHeaderADCLabel.Cursor := crHandPoint + else + FHeaderADCLabel.Cursor := crArrow; + end; +end; + +procedure TPanafallPanel.HeaderADCClick(Sender: TObject); +begin + if FADCClickable and Assigned(FOnADCClick) then FOnADCClick(Self); +end; + +procedure TPanafallPanel.BtnPopPanClick(Sender: TObject); +begin + if Assigned(FOnPanPopOut) then FOnPanPopOut(Self); +end; + procedure TPanafallPanel.ViewWindow(out VC, VS: Double); begin VC := 0; VS := 0; @@ -452,11 +531,16 @@ begin FHeaderRateLabel.Top := (H - FHeaderRateLabel.Height) div 2; FHeaderRateLabel.Left := FHeaderLabel.Left + FHeaderLabel.Width + 6; end; - if FBtnClosePan <> nil then + if (FHeaderADCLabel <> nil) and (FHeaderRateLabel <> nil) then begin - BtnS := H - 4; - FBtnClosePan.SetBounds(RW - BtnS - 2, 2, BtnS, BtnS); + FHeaderADCLabel.Top := (H - FHeaderADCLabel.Height) div 2; + FHeaderADCLabel.Left := FHeaderRateLabel.Left + FHeaderRateLabel.Width + 10; end; + BtnS := H - 4; + if FBtnClosePan <> nil then + FBtnClosePan.SetBounds(RW - BtnS - 2, 2, BtnS, BtnS); + if FBtnPopPan <> nil then + FBtnPopPan.SetBounds(RW - 2 * BtnS - 6, 2, BtnS, BtnS); end; procedure TPanafallPanel.PositionPanZoomBar(BottomY, RW, H: Integer; diff --git a/PlutoBackend.pas b/PlutoBackend.pas index d89e4f8..1e24af6 100644 --- a/PlutoBackend.pas +++ b/PlutoBackend.pas @@ -436,6 +436,7 @@ begin // RX LO, IndependentPanFreq=False — этап 3.6.) Result.MaxPans := 1; Result.IndependentPanFreq := False; + Result.NumADCs := 1; end; function TPlutoBackend.ProbeURI(const AURI: string; out Dev: TRadioDevice): Boolean; diff --git a/RadioBackend.pas b/RadioBackend.pas index df29644..a7f07ac 100644 --- a/RadioBackend.pas +++ b/RadioBackend.pas @@ -49,6 +49,9 @@ type MaxPans: Integer; // False = частоты панов связаны с паном 0 (Pluto+ 2r2t: общий RX LO). IndependentPanFreq: Boolean; + // Число АЦП (SCU): >1 → у панов есть селектор ADC-источника (A1/A2, 3.4). + // Достоверно после Connect (у HPSDR зависит от BoardType). + NumADCs: Integer; MinSampleRate: Integer; RatePresets: TBackendRateArray; // пресеты для SampleRateOverlay MinFreqHz: Double; diff --git a/RadioController.pas b/RadioController.pas index 96bf8b2..4c91bd1 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -353,6 +353,7 @@ type FPanFreqHz: array[1..MAX_PANS-1] of Double; FPanRateKHz: array[1..MAX_PANS-1] of Word; FPanADC: array[1..MAX_PANS-1] of Byte; // ADC-источник пана (3.4) + FMainADCSrc: Byte; // ADC главного DDC (пан 0) FDDCPanMap: array[0..MAX_DDCS-1] of Integer; FLastDDCSeq: LongWord; // последний seq (из сетевого потока) FLastDDCIndex: Integer; // последний DDC index @@ -576,6 +577,10 @@ type function PanDDCActive(PanId: Integer): Boolean; function PanDDCFreq(PanId: Integer): Double; function PanDDCRateKHz(PanId: Integer): Word; + // ADC-источник пана (3.4). PanId=0 = главный DDC (реконфиг ConfigureDDCs), + // N>0 = его пан-DDC (SetPanDDC). ADC ∈ [0..Caps.NumADCs-1]. + function PanADC(PanId: Integer): Byte; + function SetPanADC(PanId: Integer; ADC: Byte): Boolean; // Pluto TX-мощность через аттенюацию: drive% → дБ (потолок FPlutoTxMaxAttDb на // 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain. @@ -1317,6 +1322,35 @@ begin else Result := 0; end; +function TRadioController.PanADC(PanId: Integer): Byte; +begin + if PanId = 0 then Result := FMainADCSrc + else if PanDDCActive(PanId) then Result := FPanADC[PanId] + else Result := 0; +end; + +function TRadioController.SetPanADC(PanId: Integer; ADC: Byte): Boolean; +begin + Result := False; + if not (Assigned(FNetwork) and FRunning) then Exit; + if ADC >= FNetwork.Caps.NumADCs then Exit; + if PanId = 0 then + begin + // Главный DDC: тот же ConfigureDDCs, что при смене rate (кэш+RebuildDDCSpecific). + if FMainADCSrc = ADC then Exit(True); + FMainADCSrc := ADC; + FNetwork.ConfigureDDCs(1, FSampleRate div 1000, ADC, + FDitherEnabled, FRandomEnabled); + Exit(True); + end; + if not PanDDCActive(PanId) then Exit; + if FPanADC[PanId] = ADC then Exit(True); + FPanADC[PanId] := ADC; + FNetwork.SetPanDDC(FPanDDCIdx[PanId], True, FPanFreqHz[PanId], + FPanRateKHz[PanId], ADC); + Result := True; +end; + function TRadioController.AddSlice(TargetHz: Double; Mode, FilterLo, FilterHi: Integer; AGC: TWDSPAGCMode; Vol: Double; DevIndex: Integer; const DevName: string; APanId: Integer): Integer; @@ -3142,7 +3176,8 @@ begin // 2. Новый rate трансиверу (после остановки DSP — пакеты сразу с верным rate). if Assigned(FNetwork) and FNetwork.Connected then - FNetwork.ConfigureDDCs(1, Hz div 1000, 0, FDitherEnabled, FRandomEnabled); + FNetwork.ConfigureDDCs(1, Hz div 1000, FMainADCSrc, + FDitherEnabled, FRandomEnabled); // 3. Восстанавливаем DSP-состояние (ChangeSampleRate пересоздал канал). if Assigned(FDSPEngine) then @@ -3657,7 +3692,8 @@ begin // DDC и DUC Specific — теперь потоки эмулятора слушают на своих портах. // DUC Specific содержит mic-конфигурацию (boost/bias/line/PTT) из FTXSettings. - FNetwork.ConfigureDDCs(1, FSampleRate div 1000, 0, FDitherEnabled, FRandomEnabled); + FNetwork.ConfigureDDCs(1, FSampleRate div 1000, FMainADCSrc, + FDitherEnabled, FRandomEnabled); // Применяем сохранённое состояние динамика/отправки аудио (мьют byte 1400 bit1). FNetwork.SetSpeakerAudio(FSendAudioToRadio); // RX hw-gain в бэкенд (Pluto). HPSDR — no-op. diff --git a/SpectrumViewOpengl.pas b/SpectrumViewOpengl.pas index c7b85ab..b4f2c21 100644 --- a/SpectrumViewOpengl.pas +++ b/SpectrumViewOpengl.pas @@ -457,6 +457,11 @@ begin FBandOverlayDirty := True; FGLSpectrumW := 0; FGLSpectrumH := 0; + // Водопад — свой контекст/текстуры (история + маркер). При reparent (pop-out) + // умирают ОБА контекста; при смене MSAA только спектровый — там сброс + // водопада лишь пере-зальёт его текстуру (дёшево, история и так в CPU-буфере). + if FWaterfall is TWaterfallViewOpenGL then + TWaterfallViewOpenGL(FWaterfall).ResetGLCache; end; function TSpectrumViewOpenGL.FormatFreqGL(Hz: Double): string; diff --git a/WaterfallViewOpengl.pas b/WaterfallViewOpengl.pas index 8f5537e..02f4a07 100644 --- a/WaterfallViewOpengl.pas +++ b/WaterfallViewOpengl.pas @@ -49,6 +49,8 @@ type procedure SetTheme(const T: TAppTheme); override; procedure ResetWfBuf; override; procedure ResetWfAvgBuf; override; + // Контекст умер (reparent/пересоздание хэндла): обнулить id БЕЗ glDelete. + procedure ResetGLCache; end; implementation @@ -94,6 +96,15 @@ begin FGLControl := C; end; +procedure TWaterfallViewOpenGL.ResetGLCache; +begin + FTexture := 0; + FMarkerTex := 0; + FTexW := 0; + FTexH := 0; + FMarkerText := ''; +end; + procedure TWaterfallViewOpenGL.DeleteTexture(var Tex: GLuint); begin if Tex <> 0 then glDeleteTextures(1, @Tex); diff --git a/doc/SLICES_PLAN.md b/doc/SLICES_PLAN.md index 6901a9e..869a003 100644 --- a/doc/SLICES_PLAN.md +++ b/doc/SLICES_PLAN.md @@ -463,6 +463,21 @@ ChangeSampleRate). - Diversity (sync-DDC, оба АЦП когерентно в один канал) — НЕ здесь; протокол умеет, дизайну панов не мешает, отдельная будущая фича. +**СТАТУС 3.4 (2026-07-11): ГОТОВО, коммит 5e50cba, юзер подтвердил беглым +тестом. ⚠Ожидаются вопросы юзера по пункту «смена rate не сбрасывает АЦП».** +- `Caps.NumADCs` (HPSDR: 2 для BoardType 4/5/10 — ORION/ORION2/SATURN, те же + платы, что Pkt.NumADCs в RebuildDDCSpecific; Pluto: 1; достоверно после + Connect). +- Контроллер: `PanADC(PanId)` / `SetPanADC(PanId, ADC)`. Пан 0 = главный DDC + (`FMainADCSrc` + ConfigureDDCs с тем же rate/dither/random; FMainADCSrc + теперь прокинут и в оба штатных вызова ConfigureDDCs — смена rate не + сбрасывает АЦП). Паны N = `FPanADC[]` + SetPanDDC (RebuildDDCSpecific). +- UI: бейдж «A1»/«A2» в шапке КАЖДОГО пана (вкл. пан 0 — закрывает сценарий + rx2-support), виден только при NumADCs>1, клик = тумблер, акцентный цвет. +- ⚠Ограничение: шапка пана 0 видна только при ≥2 панах ⇒ с единственным + паном перекинуть пан 0 на ADC2 из UI нельзя (добавь пан или жди персиста). + Антенны Alex (ANT1/2/3) — по-прежнему глобально в Antenna-табе. + ### 3.5 — Pop-out: панадаптер в отдельном OS-окне Улучшение поверх Flex (SmartSDR так не умеет). Кнопка `⧉` в шапке. @@ -478,6 +493,25 @@ ChangeSampleRate). - **Риск №2 — общие GL-ресурсы**: закрывается аудитом из 3.0. - Персист: флаг «пан N в отдельном окне» + геометрия окна. +**СТАТУС 3.5 (2026-07-11): ГОТОВО, коммит 5e50cba, юзер подтвердил беглым +тестом — честный GL-reparent на Qt6/Wayland РАБОТАЕТ (fallback-пересоздание +не понадобилось).** +- Кнопка «⧉» в шапке панов N>0 → `PopOutPan`: `TForm.CreateNew` (Caption + «EWSDR · PAN N», тема, OnMouseWheel = FormMouseWheel — колесо над + флагами/фоном работает как в стеке), `TPanafallPanel.ReparentTo(F)` + (Parent всех контролов), StackTop/Height=0/0 (= всё окно), сплиттер пана + скрыт, LayoutPanStack пере-стекует оставшихся (плавающие скипаются). +- GL: reparent пересоздаёт хэндлы канв → контексты умирают. По прецеденту + MSAA: `ResetGLCache` (обнуление id без glDelete) — расширен, теперь + сбрасывает и водопад (`TWaterfallViewOpenGL.ResetGLCache`, новый); + история водопада живёт в CPU-буфере — тексура перезальётся. +- [×] окна = dock back (`CloseAction := caNone` + `DockBackPan`: reparent в + PanelRight, сброс GL, Release окна). «×» шапки в окне = закрыть пан + целиком (ClosePanadapter освобождает и окно). STOP закрывает всё. +- Резерв на случай мёртвого GL после reparent: пересоздание вьюхи/контролов + (в коде НЕ делалось — сначала экранный тест). +- Персист геометрии — вместе с общим персистом панов. + ### 3.6 — Pluto+ (2r2t): второй пан на втором антенном входе Опционально, после 3.0-3.3. Обычного Pluto НЕ касается (там один RX разведён).