feat(dxcluster): мода спота по бэндплану, когда комментарий молчит

В CW и SSB спотеры сплошь и рядом не пишут моду, и такой спот оставался
dxmUnknown: ни фильтр по модам его не видел, ни QSY по нему моду не ставил.
Теперь порядок такой: сначала комментарий (как было), и только если он молчит —
участок бэндплана.

DXSpotStore.DXModeFromFreq — таблица участков, читается сверху вниз, первое
попадание выигрывает. Узкие «водопои» цифры стоят РАНЬШЕ широких сегментов:
FT8 на 7074 живёт посреди телефонного участка R1, FT4 на 21140 — посреди
15-метрового, и без такого порядка они утонули бы в SSB.

★ Границы — по IARU Region 1 (наш регион), а споты прилетают со всего мира,
поэтому там, где регионы расходятся, мода НЕ выводится вовсе: пропуск честнее
ошибки. Отсюда дырки 1843-1850 (R1 телефон против R2 CW), 60 м (канальный),
маячные щели, 2 м/70 см кроме FT8-окон. Два спорных куска всё же отданы R1:
3570-3600 — цифре (у R2 это ещё CW, но споты там почти сплошь FT8/RTTY) и
7053-7300 — телефону (у R2 ниже 7125 данные).

QO-100 (даунлинк 10489.5-10490.0) — отдельной веткой по плану AMSAT-DL, теми же
границами, что рисует BandPlanOverlay: CW, NB/DIGI, две SSB-зоны. Маяки и
mixed modes не гадаем.

Угаданное помечено (TDXSpot.ModeGuessed) и в окне списка выводится с '?' —
'CW?' против 'CW': спотер моду не называл, а на границах участков таблица
может ошибаться, поэтому разницу видно. На спектре в чипе только позывной —
там ничего не изменилось.

Проверено консольным тестом на этих же юнитах: 25 частот + 6 полных строк
кластера (комментарий перебивает таблицу; 7012.5 → CW guessed, 7145 → SSB
guessed, 10489.680 → SSB guessed; 1846, 60 м и 144.300 остаются без моды).
Сборка ewsdr (--ws=qt6) и ewsdrd — ОК.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 18:13:28 +03:00
co-authored by Claude Opus 5
parent 17fa965aec
commit d12d8239da
3 changed files with 126 additions and 1 deletions
+113
View File
@@ -48,6 +48,7 @@ type
TimeUTC: string; // 'HHMM' как пришло от кластера
Stamp: TDateTime; // локальное время приёма — для TTL и затухания
Mode: TDXMode;
ModeGuessed: Boolean; // мода не из комментария, а из бэндплана (см. ниже)
end;
TDXSpotArray = array of TDXSpot;
@@ -94,6 +95,10 @@ type
// Мода по комментарию кластера ('FT8 -12 dB' → dxmFT8). Пусто/непонятно —
// dxmUnknown.
function DXModeFromComment(const Comment: string): TDXMode;
// Мода по участку бэндплана — запасной путь, когда комментарий молчит (в CW и
// SSB его сплошь и рядом просто не пишут). dxmUnknown = участок спорный или
// вне таблицы; см. DX_BAND_PLAN о том, где мы намеренно не гадаем.
function DXModeFromFreq(FreqHz: Double): TDXMode;
function DXModeName(M: TDXMode): string;
implementation
@@ -145,6 +150,114 @@ begin
Result := dxmDigi;
end;
{ ── Мода по бэндплану ────────────────────────────────────────────────────── }
type
TDXBandSeg = record
LoKHz, HiKHz: Double; // [Lo; Hi)
Mode: TDXMode;
end;
const
// Участки бэндплана для запасного определения моды. Читается СВЕРХУ ВНИЗ,
// первое попадание выигрывает — поэтому узкие «водопои» цифры стоят раньше
// широких сегментов, внутрь которых они попадают (FT8 на 7074 живёт посреди
// телефонного участка R1, FT4 на 21140 — посреди 15-метрового).
//
// ★ Границы — по плану IARU Region 1 (наш регион): у R2/R3 они другие, и
// спорные куски мы НАМЕРЕННО оставляем пустыми, а не гадаем — пропуск честнее
// ошибки. Отсюда дырки: 1843-1850 (R1 SSB против R2 CW), 7053-7125 у R2 —
// данные, а не телефон, и т.п. Мода из комментария всегда важнее этой
// таблицы, сюда попадают только споты, где комментарий промолчал.
DX_BAND_PLAN: array[0..47] of TDXBandSeg = (
// ── узкие окна FT8/FT4 (перекрывают широкие сегменты ниже) ──
(LoKHz: 1840.0; HiKHz: 1843.0; Mode: dxmFT8),
(LoKHz: 3573.0; HiKHz: 3576.0; Mode: dxmFT8),
(LoKHz: 7047.0; HiKHz: 7049.0; Mode: dxmFT4),
(LoKHz: 7074.0; HiKHz: 7078.0; Mode: dxmFT8),
(LoKHz: 10136.0; HiKHz: 10139.0; Mode: dxmFT8),
(LoKHz: 10140.0; HiKHz: 10142.0; Mode: dxmFT4),
(LoKHz: 14074.0; HiKHz: 14078.0; Mode: dxmFT8),
(LoKHz: 14080.0; HiKHz: 14082.0; Mode: dxmFT4),
(LoKHz: 18100.0; HiKHz: 18102.0; Mode: dxmFT8),
(LoKHz: 18104.0; HiKHz: 18106.0; Mode: dxmFT4),
(LoKHz: 21074.0; HiKHz: 21078.0; Mode: dxmFT8),
(LoKHz: 21140.0; HiKHz: 21142.0; Mode: dxmFT4),
(LoKHz: 24915.0; HiKHz: 24917.0; Mode: dxmFT8),
(LoKHz: 24919.0; HiKHz: 24921.0; Mode: dxmFT4),
(LoKHz: 28074.0; HiKHz: 28078.0; Mode: dxmFT8),
(LoKHz: 28180.0; HiKHz: 28182.0; Mode: dxmFT4),
(LoKHz: 50313.0; HiKHz: 50323.0; Mode: dxmFT8),
(LoKHz: 144174.0; HiKHz: 144180.0; Mode: dxmFT8),
(LoKHz: 432174.0; HiKHz: 432180.0; Mode: dxmFT8),
// ── КВ, широкие сегменты ──
(LoKHz: 1800.0; HiKHz: 1838.0; Mode: dxmCW),
(LoKHz: 1838.0; HiKHz: 1843.0; Mode: dxmDigi),
(LoKHz: 1850.0; HiKHz: 2000.0; Mode: dxmSSB),
(LoKHz: 3500.0; HiKHz: 3570.0; Mode: dxmCW),
(LoKHz: 3570.0; HiKHz: 3600.0; Mode: dxmDigi),
(LoKHz: 3600.0; HiKHz: 4000.0; Mode: dxmSSB),
(LoKHz: 7000.0; HiKHz: 7040.0; Mode: dxmCW),
(LoKHz: 7040.0; HiKHz: 7053.0; Mode: dxmDigi),
(LoKHz: 7053.0; HiKHz: 7300.0; Mode: dxmSSB),
(LoKHz: 10100.0; HiKHz: 10130.0; Mode: dxmCW),
(LoKHz: 10130.0; HiKHz: 10150.0; Mode: dxmDigi),
(LoKHz: 14000.0; HiKHz: 14070.0; Mode: dxmCW),
(LoKHz: 14070.0; HiKHz: 14099.0; Mode: dxmDigi),
(LoKHz: 14101.0; HiKHz: 14350.0; Mode: dxmSSB),
(LoKHz: 18068.0; HiKHz: 18095.0; Mode: dxmCW),
(LoKHz: 18095.0; HiKHz: 18109.0; Mode: dxmDigi),
(LoKHz: 18111.0; HiKHz: 18168.0; Mode: dxmSSB),
(LoKHz: 21000.0; HiKHz: 21070.0; Mode: dxmCW),
(LoKHz: 21070.0; HiKHz: 21120.0; Mode: dxmDigi),
(LoKHz: 21151.0; HiKHz: 21450.0; Mode: dxmSSB),
(LoKHz: 24890.0; HiKHz: 24915.0; Mode: dxmCW),
(LoKHz: 24915.0; HiKHz: 24929.0; Mode: dxmDigi),
(LoKHz: 24931.0; HiKHz: 24990.0; Mode: dxmSSB),
(LoKHz: 28000.0; HiKHz: 28070.0; Mode: dxmCW),
(LoKHz: 28070.0; HiKHz: 28190.0; Mode: dxmDigi),
(LoKHz: 28300.0; HiKHz: 29100.0; Mode: dxmSSB),
(LoKHz: 29100.0; HiKHz: 29700.0; Mode: dxmFM),
// ── УКВ (минимум, где регионы согласны) ──
(LoKHz: 50000.0; HiKHz: 50100.0; Mode: dxmCW),
(LoKHz: 50100.0; HiKHz: 50500.0; Mode: dxmSSB));
function DXModeFromFreq(FreqHz: Double): TDXMode;
var
K: Double;
i: Integer;
begin
Result := dxmUnknown;
if FreqHz <= 0 then Exit;
// QO-100 (узкополосный транспондер, DOWNLINK). Отдельно от КВ-таблицы: план
// AMSAT-DL, границы те же, что рисует BandPlanOverlay. Маяки (…500-505 и
// …745-755) и «mixed modes» (…850-990) — не гадаем.
K := FreqHz / 1000.0;
if (K >= 10489500.0) and (K < 10490000.0) then
begin
if (K >= 10489505.0) and (K < 10489540.0) then Result := dxmCW
else if (K >= 10489540.0) and (K < 10489650.0) then Result := dxmDigi
else if (K >= 10489650.0) and (K < 10489745.0) then Result := dxmSSB
else if (K >= 10489755.0) and (K < 10489850.0) then Result := dxmSSB;
Exit;
end;
for i := Low(DX_BAND_PLAN) to High(DX_BAND_PLAN) do
if (K >= DX_BAND_PLAN[i].LoKHz) and (K < DX_BAND_PLAN[i].HiKHz) then
Exit(DX_BAND_PLAN[i].Mode);
end;
{ ── TDXSpotStore ─────────────────────────────────────────────────────────── }
constructor TDXSpotStore.Create;