fix(web): детализация спектра/водопада — честный размер кадра, поток 'W', настройка разрешения

Веб-пульт рисовал заметно грубее десктопа, и часть причин копилась давно.

Клиент вообще не разбирал бинарный фрейм 0x57: водопад рисовался из кадра
СПЕКТРА, а собственный поток водопада (свой детектор и усреднение
wf_avg_time_ms) уходил в никуда, занимая половину трафика. Теперь водопад
идёт из своего потока.

Адаптер слал константу 1024 вместо реального числа точек. Разрешение
анализатора = ширина панадаптера, и при узкой панели (грид из двух колонок,
маленькое окно) кадр короче — хвост буфера, нули = 0 dBm, уезжал клиенту
белой полосой, а частотная шкала сжималась. В демоне это обходили
DAEMON_SPEC_WIDTH. Длина фрейма теперь равна числу заполненных точек;
клиент и так считал N из byteLength.

Децимация под потолок протокола делила «i*Count div N» — на нецелом
отношении соседние группы получались по 1 и 2 точки, а смещение
max-детектора зависит от размера группы: по спектру шла гребёнка ±3 дБ,
в водопаде — вертикальная рябь. Заменено на целочисленный фактор
(группы одинаковой ширины).

Водопад терял 2 кадра из 3: анализатор даёт до 60 кадров/с, web шлёт 20
строк/с, а бралась просто «последняя». Введено двухступенчатое накопление
max-hold (DSP-поток → PushState → PushLoop) по идиоме
TWaterfallView.SetWaterfallData — короткие посылки CW/FT8 больше не
проваливаются между строками.

Отдельно исправлено в headless: PushState демона (20 Гц) и PushLoop (20 Гц)
идут вразнобой, и на дрейфе фаз одна и та же строка уезжала клиенту дважды
(водопад двоил и полз рывками), а на остановленном RX прокручивалась
застывшая строка. Фрейм 'W' теперь отправляется только при реально пришедшем
от DSP кадре (FWfAccumFresh → WfNew → FWfFresh). Спектр так не гейтится —
это живая кривая.

Рендер в браузере: значение бина для пикселя берётся максимумом по диапазону
вместо «ближайшего» (узкие сигналы мерцали и пропадали на узком экране), при
бинах реже пикселей — интерполяция; сглаживание спектра вынесено отдельным
проходом по всем бинам (в цикле по пикселям часть бинов не обновлялась
вовсе); строка водопада рисуется из сырых бинов, IIR ~200 мс остался только
для слежения за уровнями AGC — он размазывал водопад по вертикали.

Потолок точек кадра вынесен в настройку web.spec_pixels (Settings →
Advanced → Web Server → Spectrum: 1024/2048/4096, дефолт 1024). Буферы
кадра и WS-фреймы выросли под 4096 (WEB_SPEC_MAX, синхронно с
WDSPEngine.SPECTRUM_PIXELS). В демоне эта же настройка задаёт ширину
анализатора — рендера там нет, поэтому DAEMON_SPEC_WIDTH убран.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 15:35:55 +03:00
co-authored by Claude Opus 5
parent c95620fb23
commit a820182716
8 changed files with 243 additions and 63 deletions
+37 -12
View File
@@ -66,7 +66,7 @@ type
TOnWfAGCNFChange = procedure(WfAGC, WfNF: Boolean) of object;
TOnADCChange = procedure(Dither, Random: Boolean) of object;
TOnWebSettingsChange = procedure(Enabled: Boolean; Port: Integer;
const BindAddr, User, Pass: string) of object;
const BindAddr, User, Pass: string; SpecPixels: Integer) of object;
TOnCATChange = procedure(
const SerEnabled: array of Boolean;
const SerPort: array of string;
@@ -382,6 +382,7 @@ type
FEdWebBind: TFlatEdit;
FEdWebUser: TFlatEdit;
FEdWebPass: TFlatEdit;
FCmbWebRes: TFlatComboBox;
// ---- Close button ----
FBtnClose: TFlatButton;
@@ -581,7 +582,7 @@ type
procedure LoadSpecMSAA(Samples: Integer);
procedure LoadADCSettings(Dither, Random: Boolean);
procedure LoadWebSettings(Enabled: Boolean; Port: Integer;
const BindAddr, User, Pass: string);
const BindAddr, User, Pass: string; SpecPixels: Integer);
property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange;
property OnSliceCATChange: TOnSliceCATChange read FOnSliceCATChange write FOnSliceCATChange;
@@ -2497,7 +2498,8 @@ var
Chk: TFlatCheckBox;
Ed: TFlatEdit;
Spin: TFlatSpinEdit;
Y: Integer;
Lbl: TLabel;
Y, i: Integer;
begin
with TLabel.Create(Self) do
begin
@@ -2536,7 +2538,7 @@ begin
FChkRandom := Chk;
// ── Web Server ────────────────────────────────────────────────────────────
Grp := MakeGroupPanel(FPageAdvanced, 'Web Server', MARGIN, 230, 500, 252);
Grp := MakeGroupPanel(FPageAdvanced, 'Web Server', MARGIN, 230, 500, 318);
Y := R1;
Chk := TFlatCheckBox.Create(Self);
@@ -2604,6 +2606,18 @@ begin
Ed.Text := 'hpsdr';
Ed.OnChange := OnWebAnyChange;
FEdWebPass := Ed;
// Потолок точек в кадре спектра/водопада, уходящем веб-клиенту. Реально
// отправляется min(этот потолок, разрешение анализатора = ширина
// панадаптера); выше — децимация max-детектором.
Y := Y + ROW_H;
MakeLbl(Grp, 'Spectrum:', GRP_PAD, Y + 4, LBL_W);
FCmbWebRes := MakeCombo(Grp, GRP_PAD + LBL_W + 12, Y, 130, OnWebAnyChange);
for i := 0 to High(DISPLAY_RES_NAMES) do FCmbWebRes.Items.Add(DISPLAY_RES_NAMES[i]);
FCmbWebRes.ItemIndex := 0;
Lbl := MakeLbl(Grp, '1024 px ~160 KB/s, 4096 px ~650 KB/s per client',
GRP_PAD + LBL_W + 12, Y + ROW_H - 8, 320);
Lbl.Font.Size := 8;
end;
procedure TSettingsForm.OnADCChkChange(Sender: TObject);
@@ -2614,15 +2628,21 @@ begin
end;
procedure TSettingsForm.OnWebAnyChange(Sender: TObject);
var Res: Integer;
begin
if FLoading then Exit;
if Assigned(FOnWebSettingsChange) then
FOnWebSettingsChange(
FChkWebEnabled.Checked,
FEdWebPort.Value,
FEdWebBind.Text,
FEdWebUser.Text,
FEdWebPass.Text);
if not Assigned(FOnWebSettingsChange) then Exit;
if (FCmbWebRes.ItemIndex >= 0) and (FCmbWebRes.ItemIndex <= High(DISPLAY_RES_VALUES)) then
Res := DISPLAY_RES_VALUES[FCmbWebRes.ItemIndex]
else
Res := DISPLAY_RES_VALUES[0];
FOnWebSettingsChange(
FChkWebEnabled.Checked,
FEdWebPort.Value,
FEdWebBind.Text,
FEdWebUser.Text,
FEdWebPass.Text,
Res);
end;
procedure TSettingsForm.LoadADCSettings(Dither, Random: Boolean);
@@ -2637,7 +2657,8 @@ begin
end;
procedure TSettingsForm.LoadWebSettings(Enabled: Boolean; Port: Integer;
const BindAddr, User, Pass: string);
const BindAddr, User, Pass: string; SpecPixels: Integer);
var i: Integer;
begin
FLoading := True;
try
@@ -2646,6 +2667,10 @@ begin
FEdWebBind.Text := BindAddr;
FEdWebUser.Text := User;
FEdWebPass.Text := Pass;
FCmbWebRes.ItemIndex := 0;
for i := 0 to High(DISPLAY_RES_VALUES) do
if DISPLAY_RES_VALUES[i] = SpecPixels then
begin FCmbWebRes.ItemIndex := i; Break; end;
finally
FLoading := False;
end;