mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat(dxcluster): споты DX-кластера на панадаптере
Telnet-клиент DX-кластера, база спотов и подписи позывных прямо на спектре на своих частотах. Разложено на три слоя, как бэндплан и лупа маяка. DXClusterClient.pas — один рабочий поток: резолв, неблокирующий connect с select квантами по 200 мс (Stop не ждёт таймаут соединения), логин позывным, чтение строк, реконнект с backoff. Приглашения логина И пароля ловятся в незавершённом хвосте буфера — типичный telnet-prompt приходит без CR/LF. Ошибки recv отличаются от таймаута кванта (EAGAIN/EINTR/WSAETIMEDOUT), иначе на ECONNRESET поток крутился бы в пустом цикле вместо реконнекта. Отправка дописывает частичный send. LCL-free. DXSpotStore.pas — потокобезопасная база: дедуп по позывному, TTL, потолок записей, монотонный Version. Единственная точка обмена потока с UI: никакого Synchronize, UI сам замечает правки по Version, как оверлеи — по ключам кэша. DXSpotOverlay.pas — рендер по модели BandPlanOverlay/VfoOverlay: кэшируется только полоса подписей (W × BandH) в key-color битмап, пересборка строго по dirty-ключу, на кадр — один keyed-композит. Штрихи от полосы до низа спектра рисует вызывающая сторона теми же примитивами, что и прочие маркеры: CPU — RawVLine внутри RawBegin/RawEnd, GL — DrawLine по готовому списку X/цвет. GL берёт тот же битмап текстурой и заливает её только при смене RenderVersion. Пересекающиеся подписи раскладываются лесенкой, цвет гаснет с возрастом, свой позывной выделен; палитра парная под тёмную и светлую тему. DXClusterForm.pas — окно списка: споты, лог соединения, строка команды кластеру (диалект set/filter у всех свой — не угадываем). Двойной клик или Enter = QSY. Данные тянутся поллингом по Version/LogVersion. Интеграция: кнопка DX в тулбаре (ЛКМ — подписи на спектре, ПКМ — окно), клик по подписи спота = QSY с автовыбором моды по комментарию кластера, страница SETUP → DX Cluster с персистом в секции "dxcluster". Правки SETUP прилетают посимвольно, поэтому запись конфига, TTL стора и переподключение откладываются до паузы в наборе — иначе набор позывного стоил бы шесть реконнектов, а промежуточный TTL «3» необратимо выбросил бы споты. Частота спота кладётся как есть и сравнивается с GetViewWindow: на QO-100 кластеры постят downlink 10489.xxx, что совпадает со шкалой пана само собой. Побочно в общих юнитах: WebUtils.SockSetRcvTimeout, FlatMemo.OnChange, FlatListBox.OnKeyDown, BlendBitmapKey вынесен в interface VfoOverlay (одна копия дворд-блендера на проект). Проверено на локальном фейковом кластере: логин и пароль по prompt без CR/LF, разбор спотов (включая QO-100), уход в RETRY по RST, Stop за 200 мс на висящем connect. На железе рендер не проверялся. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+32
-1
@@ -20,7 +20,7 @@ interface
|
||||
uses
|
||||
Classes, SysUtils, Graphics, GraphType, ExtCtrls, Controls, Math,
|
||||
AppTheme,
|
||||
AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay,
|
||||
AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay, DXSpotOverlay,
|
||||
WaterfallView, SMeterView, RulerView, RadioModes,
|
||||
Settings; // FilterEdgesFromBW — единственная таблица знака боковой
|
||||
|
||||
@@ -106,6 +106,7 @@ type
|
||||
FVfoOverlay: TVfoOverlay;
|
||||
FSliceOverlays: TFPList; // доп. слайс-флаги (B+); не владеет (владелец MainForm)
|
||||
FBandPlanOverlay: TBandPlanOverlay;
|
||||
FDXSpotOverlay: TDXSpotOverlay; // споты DX-кластера (полоса подписей + штрихи)
|
||||
// ── Marker ────────────────────────────────────────────────────────────────
|
||||
FMarkerActive: Boolean;
|
||||
FMarkerX: Integer;
|
||||
@@ -172,6 +173,9 @@ type
|
||||
procedure DrawSliceFilterLinesRaw(W, H: Integer);
|
||||
procedure DrawBandLetterRaw(X1, X2: Integer; L: Char; Clr: TColor);
|
||||
procedure DrawBeaconMarkersRaw(W, H: Integer);
|
||||
// Штрихи DX-спотов ниже полосы подписей. Раскладка уже посчитана оверлеем
|
||||
// при пересборке кэша — здесь только N вертикальных линий.
|
||||
procedure DrawDXSpotTicksRaw(W, H: Integer);
|
||||
// 2TON/IMD: замер пиков по TX-буферу (общий для CPU/GL рендеров).
|
||||
// Возвращает число валидных маркеров в M (0 если измерять нечего):
|
||||
// [0..1] тона, [2..3] IMD3 (2f1−f2, 2f2−f1). Обновляет FIMDSummary.
|
||||
@@ -318,6 +322,7 @@ type
|
||||
function SliceOverlayCount: Integer;
|
||||
function SliceOverlayAt(Index: Integer): TVfoOverlay;
|
||||
property BandPlanOverlay: TBandPlanOverlay read FBandPlanOverlay write FBandPlanOverlay;
|
||||
property DXSpotOverlay: TDXSpotOverlay read FDXSpotOverlay write FDXSpotOverlay;
|
||||
|
||||
// ── Данные от DSP ─────────────────────────────────────────────────────────
|
||||
procedure SetSpectrumData(const Pixels: array of Single; Count: Integer); virtual;
|
||||
@@ -669,6 +674,23 @@ begin
|
||||
FBeaconDecHalf := HalfHz;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.DrawDXSpotTicksRaw(W, H: Integer);
|
||||
// Штрих от низа полосы подписей до низа спектра — по одному на видимый спот.
|
||||
// Пунктиром, чтобы не спорить с кривой сигнала и краями фильтра. Раскладка
|
||||
// готова (EnsureRendered в начале кадра), здесь только N линий.
|
||||
var
|
||||
i, X: Integer;
|
||||
Col: TColor;
|
||||
begin
|
||||
if (FDXSpotOverlay = nil) or (not FDXSpotOverlay.Active) then Exit;
|
||||
for i := 0 to FDXSpotOverlay.TickCount - 1 do
|
||||
begin
|
||||
FDXSpotOverlay.Tick(i, X, Col);
|
||||
if (X < 0) or (X >= W) then Continue;
|
||||
RawVLine(X, FDXSpotOverlay.BandHeight, H - 1, Col, 1, 2, 3);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.DrawBeaconMarkersRaw(W, H: Integer);
|
||||
// Две вертикали: опорная частота маяка (зелёная пунктир) и отслеживаемый
|
||||
// центроид (оранжевая сплошная). Расхождение видно глазом → понятно, сел ли
|
||||
@@ -1705,6 +1727,10 @@ begin
|
||||
FSpPts[W] := Point(W-1, H); FSpPts[W+1] := Point(0, H);
|
||||
|
||||
|
||||
// Раскладка спотов — ДО RawBegin: штрихи в фазе raw берут из неё готовые X,
|
||||
// а сама пересборка идёт на своём кэш-битмапе и только по dirty-ключу.
|
||||
if Assigned(FDXSpotOverlay) then FDXSpotOverlay.EnsureRendered(W);
|
||||
|
||||
// ═══ Фаза 1: raw — весь кадр одним локом ═════════════════════════════════
|
||||
{$IFDEF DARWIN}
|
||||
// Cocoa-вариант CopyGridToSpectrum идёт через Canvas.Draw — до RawBegin.
|
||||
@@ -1794,6 +1820,8 @@ begin
|
||||
|
||||
if FMarkerActive then DrawMarkerLineRaw(W, H);
|
||||
if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkersRaw(W, H);
|
||||
// Штрихи DX-спотов — под маркерами наведения, но над кривой.
|
||||
DrawDXSpotTicksRaw(W, H);
|
||||
// 2TON: кружки с уровнями на пиках тонов/IMD3 (активирует MainForm при
|
||||
// 2TON+TX+DUP — реальный сигнал после PA виден на RX-спектре)
|
||||
if FIMDActive then DrawIMDMarkersRaw(W, H, DBmax, InvRange);
|
||||
@@ -1809,6 +1837,9 @@ begin
|
||||
// Бэндплан QO-100 — полоска внизу спектра, под панелями оверлеев.
|
||||
if Assigned(FBandPlanOverlay) then
|
||||
FBandPlanOverlay.DrawOverlay(FSpectrumBitmap, W, H);
|
||||
// Полоса подписей DX-спотов — вверху спектра, под флагами VFO.
|
||||
if Assigned(FDXSpotOverlay) then
|
||||
FDXSpotOverlay.DrawOverlay(FSpectrumBitmap, W, H);
|
||||
if Assigned(FSampleRateOverlay) then
|
||||
FSampleRateOverlay.DrawOverlay(FSpectrumBitmap, C, W, H);
|
||||
if Assigned(FVfoOverlay) then
|
||||
|
||||
Reference in New Issue
Block a user