feat: QO-100 NB transponder bandplan overlay on spectrum

Add a thin colored strip along the bottom of the spectrum (above the
ruler) showing the QO-100 narrow-band transponder bandplan: mode
segments (CW ONLY / NB DIGI / DIGI / SSB / MIXED MODES) plus the three
beacons (CW / PSK / EXP) as muted-red cells at their ranges.

- New unit BandPlanOverlay.pas (TBandPlanOverlay), modelled on
  VfoOverlay: content rendered once into a cache bitmap, per-frame cost
  is just a constant-alpha composite (CPU) / one textured quad (GL).
  Cache invalidates only on view change (center/span/width) so it idles
  while the QO-100 downlink display is static.
- Wired into both render paths: SpectrumView (CPU composite) and
  SpectrumViewOpengl (texture, re-uploaded only when dirty).
- MainForm: SetView fed from SyncSpecViewFreq; visibility gated by
  InQO100 (Pluto + full-duplex transponder), like RX MUTE / BEACON.
- DPI-robust: strip height scales with Screen.PixelsPerInch and the font
  height is tied to the strip height (not point size), so labels always
  fit at any Windows scaling.
- Segment/beacon frequencies and colors are a single editable table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 21:02:50 +03:00
co-authored by Claude Opus 4.8
parent 3bd37b956e
commit 432c0285f0
5 changed files with 382 additions and 6 deletions
+310
View File
@@ -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.50010490.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.
+22 -1
View File
@@ -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
// ---------------------------------------------------------------------------
+6 -1
View File
@@ -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
+33 -1
View File
@@ -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);
+11 -3
View File
@@ -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`