diff --git a/BandPlanOverlay.pas b/BandPlanOverlay.pas new file mode 100644 index 0000000..e8b93a2 --- /dev/null +++ b/BandPlanOverlay.pas @@ -0,0 +1,310 @@ +unit BandPlanOverlay; + +{ + Оверлей бэндплана QO-100 (узкополосный транспондер) — тонкая цветная полоска + вдоль оси частот ВНИЗУ спектра (над ruler) с подписями сегментов. + + Производительность — модель VfoOverlay: контент рендерится в кэш-битмап ОДИН раз + и пересобирается только при смене вида (center/span/размер/тема/enabled), а каждый + кадр идёт лишь композит полоски (W × STRIP_H) с постоянной альфой. Дорогих + попиксельных проходов на кадр нет. GL-путь берёт тот же битмап текстурой + (DrawOverlayBitmap) и заливает её только при dirty — одинаковый вид в CPU и GL, + т.к. GL-UploadBitmap поддерживает color-key + постоянную альфу (как у VfoOverlay). + + Сегменты заданы таблицей в DOWNLINK-Гц (QO100_NB). Границы — по плану AMSAT-DL + NB-транспондера; правятся в одном месте. +} + +{$mode objfpc}{$H+} + +interface + +uses + Classes, SysUtils, Controls, Graphics, Types, Math, Forms; + +const + BANDPLAN_STRIP_BASE_H = 20; // базовая высота полоски @96dpi (чуть больше ruler=18); + // фактическая FStripH масштабируется по DPI экрана + BANDPLAN_ALPHA = 188; // глобальная альфа композита (0..255) + +type + // Тип сегмента → цвет. Правится таблицей SEG_COLOR ниже. + TSegKind = (skBeacon, skCW, skSSB, skDigi, skNBDigi, skDV, skMixed); + + TBandSeg = record + Lo, Hi: Double; // downlink-Гц + Name: string; + Kind: TSegKind; + end; + + TBandPlanOverlay = class(TComponent) + private + FCenterFreq: Double; + FSpanHz: Double; + FEnabled: Boolean; + + FCache: TBitmap; // W × STRIP_H, непрозрачный; альфа применяется при композите + FCacheW: Integer; + FCacheDirty: Boolean; + // ключ кэша — пересборка только при изменении + FKeyCenter: Double; + FKeySpan: Double; + FKeyW: Integer; + FKeyEnabled: Boolean; + FStripH: Integer; // фактическая высота полоски (px), масштаб по DPI + + function FreqToX(FreqHz: Double; W: Integer): Integer; + procedure DrawSelf(C: TCanvas; W: Integer); + procedure RebuildCache(W: Integer); + function NeedsRebuild(W: Integer): Boolean; + procedure CompositeStrip(Target: TBitmap; DstY: Integer); + public + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + // Текущий вид спектра (downlink-домен). Дёргается при тюнинге/смене span. + procedure SetView(ACenterHz, ASpanHz: Double); + procedure SetEnabled(En: Boolean); + function Active: Boolean; + // CPU-путь: композит полоски внизу Target (над ruler). + procedure DrawOverlay(Target: TBitmap; W, H: Integer); + // GL-путь: рендер полоски в Target-битмап (W × STRIP_H) для заливки текстурой. + procedure DrawOverlayBitmap(Target: TBitmap; W: Integer); + property Enabled: Boolean read FEnabled; + property CenterFreq: Double read FCenterFreq; // для GL-инвалидизации текстуры + property SpanHz: Double read FSpanHz; + end; + +implementation + +const + // Палитра по типу сегмента (TColor = $00BBGGRR). + SEG_COLOR: array[TSegKind] of TColor = ( + $005258C8, // skBeacon — приглушённый красный (с прохладным оттенком палитры) + $00C07820, // skCW — синий + $002F9A3A, // skSSB — зелёный + $00B0479E, // skDigi — фиолетовый (яркий) + $00794A6E, // skNBDigi — приглушённый фиолетовый (NB DIGI, тусклее DIGI) + $009A9A20, // skDV — бирюзовый + $00606060); // skMixed — серый + + CLR_STRIP_BG = TColor($00181818); // фон полоски вне транспондера + CLR_STRIP_TOP = TColor($00585858); // верхняя кромка (отделить от спектра) + CLR_SEP = TColor($000C0C0C); // разделители сегментов + CLR_LBL = TColor($00F0F0F0); // подписи + + // ── Бэндплан QO-100 NB-транспондера (DOWNLINK Гц) ──────────────────────────── + // Транспондер 10489.500–10490.000 (500 кГц). Mode-заливки между маяками; маяки — + // отдельными ярко-янтарными ячейками на своих диапазонах. Границы правятся ЗДЕСЬ. + QO100_NB: array[0..5] of TBandSeg = ( + (Lo: 10489505000.0; Hi: 10489540000.0; Name: 'CW ONLY'; Kind: skCW), + (Lo: 10489540000.0; Hi: 10489580000.0; Name: 'NB DIGI'; Kind: skNBDigi), + (Lo: 10489580000.0; Hi: 10489650000.0; Name: 'DIGI'; Kind: skDigi), + (Lo: 10489650000.0; Hi: 10489745000.0; Name: 'SSB'; Kind: skSSB), + (Lo: 10489755000.0; Hi: 10489850000.0; Name: 'SSB'; Kind: skSSB), + (Lo: 10489850000.0; Hi: 10489990000.0; Name: 'MIXED MODES'; Kind: skDV)); + + // Маяки QO-100 (диапазоны): нижний CW, средний PSK, верхний. Ярко-янтарные ячейки. + QO100_BEACONS: array[0..2] of TBandSeg = ( + (Lo: 10489500000.0; Hi: 10489505000.0; Name: 'CW'; Kind: skBeacon), // нижний CW-маяк + (Lo: 10489745000.0; Hi: 10489755000.0; Name: 'PSK'; Kind: skBeacon), // средний PSK-маяк + (Lo: 10489990000.0; Hi: 10490000000.0; Name: 'EXP'; Kind: skBeacon)); // верхний эксп. маяк + +function Darken(C: TColor; Num, Den: Integer): TColor; +var r, g, b: Integer; +begin + r := (C and $FF) * Num div Den; + g := ((C shr 8) and $FF) * Num div Den; + b := ((C shr 16) and $FF) * Num div Den; + Result := TColor((b shl 16) or (g shl 8) or r); +end; + +constructor TBandPlanOverlay.Create(AOwner: TComponent); +begin + inherited Create(AOwner); + // Высота полоски масштабируется по DPI экрана (как RULER_H в MainForm), иначе на + // Windows-масштабе 125/150/200% полоска осталась бы фиксированно тонкой. + FStripH := Round(BANDPLAN_STRIP_BASE_H * Screen.PixelsPerInch / 96); + if FStripH < BANDPLAN_STRIP_BASE_H then FStripH := BANDPLAN_STRIP_BASE_H; + FCenterFreq := 10489750000.0; + FSpanHz := 500000.0; + FEnabled := False; + FCache := TBitmap.Create; + FCache.PixelFormat := pf32bit; + FCacheW := 0; + FCacheDirty := True; + FKeyCenter := -1; FKeySpan := -1; FKeyW := -1; FKeyEnabled := False; +end; + +destructor TBandPlanOverlay.Destroy; +begin + FCache.Free; + inherited Destroy; +end; + +procedure TBandPlanOverlay.SetView(ACenterHz, ASpanHz: Double); +begin + // Квантуем сравнение, чтобы микродрейф не вызывал постоянную пересборку. + if (Abs(ACenterHz - FCenterFreq) < 1.0) and (Abs(ASpanHz - FSpanHz) < 1.0) then Exit; + FCenterFreq := ACenterHz; + FSpanHz := ASpanHz; + FCacheDirty := True; +end; + +procedure TBandPlanOverlay.SetEnabled(En: Boolean); +begin + if FEnabled = En then Exit; + FEnabled := En; + FCacheDirty := True; +end; + +function TBandPlanOverlay.Active: Boolean; +begin + Result := FEnabled; +end; + +function TBandPlanOverlay.FreqToX(FreqHz: Double; W: Integer): Integer; +begin + if FSpanHz <= 0 then Result := -1 + else Result := Round((FreqHz - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W); +end; + +procedure TBandPlanOverlay.DrawSelf(C: TCanvas; W: Integer); +// Рендерит полоску в канвас битмапа W × FStripH, начало (0,0), НЕПРОЗРАЧНО. +// Прозрачность накладывается позже (CompositeStrip / GL FixedAlpha). +var + i, ty, th, SH: Integer; + + // Единый стиль для всех ячеек (mode-сегменты И маяки): приглушённая заливка + + // яркая кромка цвета сверху + разделитель слева + подпись по центру если влезает. + procedure DrawSeg(const seg: TBandSeg); + var x1, x2, cx, tw: Integer; col: TColor; + begin + x1 := FreqToX(seg.Lo, W); + x2 := FreqToX(seg.Hi, W); + if (x2 <= 0) or (x1 >= W) then Exit; // вне экрана + if x1 < 0 then x1 := 0; + if x2 > W then x2 := W; + if x2 <= x1 then x2 := x1 + 1; // минимум 1px видимости (узкие маяки) + + col := SEG_COLOR[seg.Kind]; + C.Brush.Color := Darken(col, 3, 5); C.Brush.Style := bsSolid; C.Pen.Style := psClear; + C.FillRect(Rect(x1, 1, x2, SH)); + C.Brush.Color := col; // яркая кромка-«легенда» сверху + C.FillRect(Rect(x1, 1, x2, 3)); + + C.Pen.Style := psSolid; C.Pen.Color := CLR_SEP; C.Pen.Width := 1; + C.MoveTo(x1, 1); C.LineTo(x1, SH); + + tw := C.TextWidth(seg.Name); + if (x2 - x1) >= tw + 6 then + begin + cx := x1 + ((x2 - x1) - tw) div 2; + C.Brush.Style := bsClear; C.Font.Color := CLR_LBL; + C.TextOut(cx, ty, seg.Name); + end; + end; + +begin + SH := FStripH; + + // Фон всей полоски (зона вне транспондера остаётся этим тёмным фоном). + C.Brush.Color := CLR_STRIP_BG; C.Brush.Style := bsSolid; C.Pen.Style := psClear; + C.FillRect(Rect(0, 0, W, SH)); + + // Высота шрифта привязана к высоте полоски (а не point-size), поэтому текст + // всегда влезает при любом DPI-масштабе Windows. Отриц. Height = glyph-px. + C.Font.Name := 'Courier New'; C.Font.Style := []; + C.Font.Height := -Max(8, Round(SH * 0.5)); + th := C.TextHeight('BCN'); + // Вертикальный центр под caps: +1 компенсирует пустой descent у заглавных. + ty := (SH - th) div 2 + 1; + if ty < 1 then ty := 1; + + // Mode-сегменты, затем маяки (поверх) — одинаковый стиль. + for i := Low(QO100_NB) to High(QO100_NB) do DrawSeg(QO100_NB[i]); + for i := Low(QO100_BEACONS) to High(QO100_BEACONS) do DrawSeg(QO100_BEACONS[i]); + + // Верхняя кромка полоски — отделить от спектра. + C.Pen.Style := psSolid; C.Pen.Color := CLR_STRIP_TOP; C.Pen.Width := 1; + C.MoveTo(0, 0); C.LineTo(W, 0); + C.Pen.Style := psSolid; +end; + +function TBandPlanOverlay.NeedsRebuild(W: Integer): Boolean; +begin + Result := FCacheDirty or (FCacheW <> W) + or (FKeyW <> W) or (FKeyEnabled <> FEnabled) + or (Abs(FKeyCenter - FCenterFreq) >= 1.0) + or (Abs(FKeySpan - FSpanHz) >= 1.0); +end; + +procedure TBandPlanOverlay.RebuildCache(W: Integer); +begin + if W <= 0 then begin FCacheW := 0; FCacheDirty := False; Exit; end; + if (FCache.Width <> W) or (FCache.Height <> FStripH) then + FCache.SetSize(W, FStripH); + DrawSelf(FCache.Canvas, W); + FCacheW := W; + FKeyCenter := FCenterFreq; FKeySpan := FSpanHz; FKeyW := W; + FKeyEnabled := FEnabled; + FCacheDirty := False; +end; + +procedure TBandPlanOverlay.CompositeStrip(Target: TBitmap; DstY: Integer); +// Альфа-композит FCache поверх Target в (0, DstY) с постоянной BANDPLAN_ALPHA. +// Один проход по W × STRIP_H пикселей за кадр. +var + X, Y, ty, InvA: Integer; + RS, RD: PByte; +const + A = BANDPLAN_ALPHA; +begin + if (Target = nil) or (FCacheW <= 0) then Exit; + InvA := 255 - A; + Target.BeginUpdate(False); + FCache.BeginUpdate(False); + try + for Y := 0 to FStripH - 1 do + begin + ty := DstY + Y; + if (ty < 0) or (ty >= Target.Height) then Continue; + RS := PByte(FCache.ScanLine[Y]); + RD := PByte(Target.ScanLine[ty]); + if (RS = nil) or (RD = nil) then Continue; + for X := 0 to FCacheW - 1 do + begin + if X >= Target.Width then Break; +{$IFDEF DARWIN} + RD[1] := Byte((A * RS[1] + InvA * RD[1]) div 255); + RD[2] := Byte((A * RS[2] + InvA * RD[2]) div 255); + RD[3] := Byte((A * RS[3] + InvA * RD[3]) div 255); +{$ELSE} + RD[0] := Byte((A * RS[0] + InvA * RD[0]) div 255); + RD[1] := Byte((A * RS[1] + InvA * RD[1]) div 255); + RD[2] := Byte((A * RS[2] + InvA * RD[2]) div 255); +{$ENDIF} + Inc(RS, 4); Inc(RD, 4); + end; + end; + finally + FCache.EndUpdate(False); + Target.EndUpdate(False); + end; +end; + +procedure TBandPlanOverlay.DrawOverlay(Target: TBitmap; W, H: Integer); +begin + if (not FEnabled) or (Target = nil) or (W <= 0) or (H <= FStripH) then Exit; + if NeedsRebuild(W) then RebuildCache(W); + CompositeStrip(Target, H - FStripH); +end; + +procedure TBandPlanOverlay.DrawOverlayBitmap(Target: TBitmap; W: Integer); +begin + if (Target = nil) or (W <= 0) then Exit; + Target.PixelFormat := pf32bit; + Target.SetSize(W, FStripH); + DrawSelf(Target.Canvas, W); +end; + +end. diff --git a/MainForm.pas b/MainForm.pas index 867c731..c5016b5 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -22,7 +22,7 @@ unit MainForm; interface uses - Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, SampleRateOverlay, + Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, SampleRateOverlay, BandPlanOverlay, FlatButton, FlatSlider, FlatDropDown, AppTheme, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls, Buttons, Menus, Math, Types, OpenGLContext, @@ -123,6 +123,7 @@ type FPendingIP: string; // IP устройства для подключения FVfoOverlay: TVfoOverlay; // накладка SmartSDR-стиль на спектре FSampleRateOverlay: TSampleRateOverlay; // оверлей span/hide слева вверху спектра + FBandPlanOverlay: TBandPlanOverlay; // полоска бэндплана QO-100 внизу спектра FPanelHidden: Boolean; // True = левая панель скрыта FLeftPanelW: Integer; // ширина панели до скрытия (DPI-safe restore) FPendingBoardType: Integer; // BoardType устройства для подключения @@ -390,6 +391,7 @@ type procedure ApplyDUP(Active: Boolean); procedure BtnRxMuteClick(Sender: TObject); procedure UpdateRxMuteButton; // видимость (Pluto+full-duplex) + стиль кнопки RX MUTE + procedure UpdateBandPlanOverlay; // вкл/выкл полоски бэндплана QO-100 по InQO100 procedure BtnTUNClick(Sender: TObject); procedure ApplyTUN(Active: Boolean); procedure UpdateFilterButtons; @@ -1221,6 +1223,7 @@ begin UpdateBeaconButton; end; UpdateRxMuteButton; // RX MUTE — только Pluto + full-duplex (QO-100) + UpdateBandPlanOverlay; // полоска бэндплана QO-100 // Wideband-вид + web-зеркало xvtr (ранее в обёртках Activate/Deactivate). // rfXvtr эмитят ТОЛЬКО ActivateXvtr/DeactivateXvtr — состояние финальное. UpdateWidebandFrequencyView; @@ -2085,6 +2088,11 @@ begin FSampleRateOverlay.SetCurrentRate(FController.FSampleRate); FSpecView.SampleRateOverlay := FSampleRateOverlay; + // Бэндплан QO-100 — полоска внизу спектра; видимость по InQO100. + FBandPlanOverlay := TBandPlanOverlay.Create(Self); + FBandPlanOverlay.SetView(FController.FCenterFreq, FController.FSpanHz); + FSpecView.BandPlanOverlay := FBandPlanOverlay; + // S-метр — внутри PanelToolbar, справа PanelSMeterRight := TPanel.Create(Self); PanelSMeterRight.Parent := PanelToolbar; @@ -3002,6 +3010,10 @@ begin // FSplitTxB (TX-A/TX-B), независимо от активного RX-VFO. if FController.FSplitTxB then FSpecView.TXVfoIndex := 1 else FSpecView.TXVfoIndex := 0; + // Бэндплан QO-100 следует за видом спектра (downlink-домен). SetView сам + // отсекает неизменное → пересборки кэша на каждый вызов нет. + if Assigned(FBandPlanOverlay) then + FBandPlanOverlay.SetView(FController.FCenterFreq, FController.FSpanHz); if UpdateWidebandFrequencyView and FController.FShowWideband and (PbWideband <> nil) then PbWideband.Invalidate; end; @@ -3715,6 +3727,7 @@ begin LayoutLeftPanel(Caps.HasHWGain); if Caps.HasHWGain then UpdateRxGainUI; UpdateRxMuteButton; // RX MUTE — видимость по Pluto + full-duplex (если QO-100 уже активен) + UpdateBandPlanOverlay; // полоска бэндплана QO-100 end; procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice); @@ -4066,6 +4079,14 @@ begin if Vis then StyleButton(BtnRxMute, FController.FRxMuteOnTx); end; +procedure TMainForm.UpdateBandPlanOverlay; +// Полоска бэндплана видна только в QO-100 (Pluto + full-duplex транспондер). +begin + if FBandPlanOverlay = nil then Exit; + FBandPlanOverlay.SetEnabled(FController.InQO100); + FSpectrumDirty := True; // моментальная перерисовка (не ждём таймер) +end; + // --------------------------------------------------------------------------- // TUN — Thetis-style тон: WDSP TXA PostGen + MOX с отдельным уровнем Drive // --------------------------------------------------------------------------- diff --git a/SpectrumView.pas b/SpectrumView.pas index f729828..dd90f78 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -20,7 +20,7 @@ interface uses Classes, SysUtils, Graphics, ExtCtrls, Controls, Math, AppTheme, - AlertOverlay, SampleRateOverlay, VfoOverlay, + AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay, WaterfallView, SMeterView, RulerView; const @@ -70,6 +70,7 @@ type FADCOverloadVisible: Boolean; FSampleRateOverlay: TSampleRateOverlay; FVfoOverlay: TVfoOverlay; + FBandPlanOverlay: TBandPlanOverlay; // ── Marker ──────────────────────────────────────────────────────────────── FMarkerActive: Boolean; FMarkerX: Integer; @@ -217,6 +218,7 @@ type property PbSMeterRight: TPaintBox write SetPbSMeterRight; property SampleRateOverlay: TSampleRateOverlay read FSampleRateOverlay write FSampleRateOverlay; property VfoOverlay: TVfoOverlay read FVfoOverlay write FVfoOverlay; + property BandPlanOverlay: TBandPlanOverlay read FBandPlanOverlay write FBandPlanOverlay; // ── Данные от DSP ───────────────────────────────────────────────────────── procedure SetSpectrumData(const Pixels: array of Single; Count: Integer); virtual; @@ -1000,6 +1002,9 @@ begin if FMarkerActive then DrawMarkerLine(C, W, H); if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkers(C, W, H); DrawADCOverloadOverlay(C, W, H); + // Бэндплан QO-100 — полоска внизу спектра (над ruler), под панелями оверлеев. + if Assigned(FBandPlanOverlay) then + FBandPlanOverlay.DrawOverlay(FSpectrumBitmap, W, H); if Assigned(FSampleRateOverlay) then FSampleRateOverlay.DrawOverlay(FSpectrumBitmap, C, W, H); if Assigned(FVfoOverlay) then diff --git a/SpectrumViewOpengl.pas b/SpectrumViewOpengl.pas index 1dc1391..941f38c 100644 --- a/SpectrumViewOpengl.pas +++ b/SpectrumViewOpengl.pas @@ -17,7 +17,7 @@ interface uses Classes, SysUtils, Graphics, Controls, Math, Types, OpenGLContext, GL, - AppTheme, SpectrumView, VfoOverlay, WaterfallViewOpengl; + AppTheme, SpectrumView, VfoOverlay, BandPlanOverlay, WaterfallViewOpengl; type TGLTextureCache = record @@ -34,6 +34,7 @@ type FOverlayDirty: Boolean; FSampleOverlayDirty: Boolean; FVfoOverlayDirty: Boolean; + FBandOverlayDirty: Boolean; FGridLabelTex: TGLTextureCache; FAGCLabelTex: TGLTextureCache; FAGCHangLabelTex: TGLTextureCache; @@ -41,6 +42,7 @@ type FADCOverlayTex: TGLTextureCache; FSampleOverlayTex: TGLTextureCache; FVfoOverlayTex: TGLTextureCache; + FBandOverlayTex: TGLTextureCache; FLastAGCText: string; FLastAGCY: Integer; FLastAGCHangY: Integer; @@ -55,6 +57,9 @@ type FLastVfoY: Integer; FLastVfoW: Integer; FLastVfoH: Integer; + FLastBandW: Integer; + FLastBandCenter: Double; + FLastBandSpan: Double; FGLSpectrumW: Integer; FGLSpectrumH: Integer; FSpY: array of Integer; @@ -123,6 +128,9 @@ begin FOverlayDirty := True; FSampleOverlayDirty := True; FVfoOverlayDirty := True; + FBandOverlayDirty := True; + FLastBandW := -MaxInt; + FLastBandCenter := -1; FLastBandSpan := -1; FLastAGCY := -MaxInt; FLastAGCHangY := -MaxInt; FLastMarkerX := -MaxInt; @@ -140,6 +148,7 @@ begin DeleteTexture(FADCOverlayTex); DeleteTexture(FSampleOverlayTex); DeleteTexture(FVfoOverlayTex); + DeleteTexture(FBandOverlayTex); end; inherited Destroy; end; @@ -720,6 +729,29 @@ var B: TBitmap; OW, OH: Integer; begin + // Бэндплан QO-100 — полоска внизу спектра (над ruler), под панелями оверлеев. + if Assigned(BandPlanOverlay) and BandPlanOverlay.Active then + begin + if FOverlayDirty or FBandOverlayDirty or FBandOverlayTex.Dirty or + (FLastBandW <> W) or + (Abs(FLastBandCenter - BandPlanOverlay.CenterFreq) >= 1.0) or + (Abs(FLastBandSpan - BandPlanOverlay.SpanHz) >= 1.0) then + begin + B := TBitmap.Create; + try + BandPlanOverlay.DrawOverlayBitmap(B, W); + UploadBitmap(FBandOverlayTex, B, False, BANDPLAN_ALPHA); + finally + B.Free; + end; + FLastBandW := W; + FLastBandCenter := BandPlanOverlay.CenterFreq; + FLastBandSpan := BandPlanOverlay.SpanHz; + FBandOverlayDirty := False; + end; + DrawTexture(FBandOverlayTex, 0, H - FBandOverlayTex.H); + end; + if Assigned(FSampleRateOverlay) then begin OW := Min(FSampleRateOverlay.Width, W - FSampleRateOverlay.Left); diff --git a/doc/PLUTO_INTEGRATION_PLAN.md b/doc/PLUTO_INTEGRATION_PLAN.md index c5dd129..5765b31 100644 --- a/doc/PLUTO_INTEGRATION_PLAN.md +++ b/doc/PLUTO_INTEGRATION_PLAN.md @@ -343,9 +343,17 @@ end; downlink через транспондер, ~250 мс). Реализация: гейт `WDSPEngine` (`not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX)`) + флаг `FRxMuteOnTx` в контроллере, персист в глобале (`rx_mute_on_tx`). Web — не сделано (пока вне области). -- TODO: проверка на железе; RIT/XIT-клариффайер (опц., на геостационаре редко нужен); - TX-маркер на RX-спектре в DUP (визуальный self-monitor — `FSpecView.TXMode` сейчас - в DUP выключен). +- ✅ **Bandplan-оверлей NB-транспондера** (`BandPlanOverlay.pas`): полоска внизу + спектра (над ruler) с сегментами CW ONLY / NB DIGI / DIGI / SSB / MIXED MODES + + маяки CW/PSK/EXP. Модель `VfoOverlay` (кэш-битмап + const-alpha), CPU и GL, виден + по `InQO100`. DPI-устойчив (высота и шрифт по `Screen.PixelsPerInch`). +- TODO (визуальный self-monitor / навигация по транспондеру): + * **Индикатор «не громче маяка»** — сравнение уровня своего downlink с маяком + (этикет QO-100); амплитуда маяка уже известна декодеру. + * RIT/XIT-клариффайер — опц., на геостационаре нужен редко. +- TODO: проверка на железе. +- ОТВЕРГНУТО: TX-маркер на RX-спектре в DUP — в обычной работе RX=TX (один VFO), + маркер ложится на RX-курсор, информации не даёт; полезен только в split (редко). **Фаза 6 — Полировка** ✅ (в основном) - ✅ Web/CAT-фронтенды (команды уже агностичны) + гейтинг полей; демон `ewsdrd.lpr`