feat: unify band plan across desktop + web UIs

Web band selector was hardcoded to the HF plan, so on Pluto it showed
160m..6m instead of the VHF/UHF plan and highlighted the wrong entry
(band switching itself already worked: web sends idx, controller maps it
via the IsPluto plan). Mirror the sample-rate unification.

Single source of truth = BoardUtils -> controller:
- BoardUtils: TBandInfo/TBandPlanArray + BuildBandPlan(Pluto).
- TRadioController.BandPlan returns BuildBandPlan(IsPluto).

Both UIs derive from it:
- Desktop already plan-aware (RelabelBands + RestoreBand) - unchanged.
- Web: WebServer.SetBands + "bands" in state JSON; hosts push
  BandPlan on connect/startup; JS builds the band selector from
  state.bands (BAND_N/BAND_F now dynamic).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 14:01:23 +03:00
co-authored by Claude Opus 4.8
parent 6b695ce756
commit 4749cf0cbc
6 changed files with 127 additions and 7 deletions
+30
View File
@@ -6,6 +6,15 @@ unit BoardUtils;
interface
type
// Один диапазон плана (имя кнопки + частота по умолчанию). Общий тип для
// desktop/web UI — единый источник band-плана через TRadioController.BandPlan.
TBandInfo = record
Name: string;
FreqHz: Double;
end;
TBandPlanArray = array of TBandInfo;
function BoardTypeName(BoardType: Integer): string;
// Короткое человекочитаемое имя AD936x-платы по hw_model (Pluto / LibreSDR / прочие).
function PlutoModelName(const HwModel: string): string;
@@ -15,6 +24,8 @@ function FreqToBandIdx(Hz: Double; Pluto: Boolean = False): Integer;
function BandName(Idx: Integer; Pluto: Boolean = False): string;
// Частота по умолчанию при входе в диапазон (если нет сохранённой/в диапазоне).
function BandDefaultFreq(Idx: Integer; Pluto: Boolean = False): Double;
// Полный band-план активного бэкенда (имена + дефолтные частоты).
function BuildBandPlan(Pluto: Boolean): TBandPlanArray;
implementation
@@ -105,4 +116,23 @@ begin
if Pluto then Result := VU_DEF[Idx] else Result := HF_DEF[Idx];
end;
function BuildBandPlan(Pluto: Boolean): TBandPlanArray;
var i: Integer;
begin
SetLength(Result, BAND_COUNT);
for i := 0 to BAND_COUNT - 1 do
begin
if Pluto then
begin
Result[i].Name := VU_NAMES[i];
Result[i].FreqHz := VU_DEF[i];
end
else
begin
Result[i].Name := HF_NAMES[i];
Result[i].FreqHz := HF_DEF[i];
end;
end;
end;
end.
+24
View File
@@ -479,6 +479,7 @@ type
procedure OnXvtrSettingsChange(const X: TXvtrSettings);
procedure PushXvtrToWeb;
procedure PushRatePresetsToWeb; // backend sample-rate пресеты → web
procedure PushBandsToWeb; // backend band-план → web
// XVTR helpers
function FindXvtrIdxForFreq(FreqHz: Double): Integer;
function XvtrTranslate(VisibleHz: Double): Double;
@@ -884,6 +885,7 @@ begin
PushXvtrToWeb;
PushDeviceListToWeb; // saved-устройства сразу видны web-overlay
PushRatePresetsToWeb; // дефолтный бэкенд до connect; перезапишется на смене
PushBandsToWeb;
FillChar(FCATLastGlobal, SizeOf(FCATLastGlobal), 0);
FCATLastGlobal.CATTcpPort := 19090;
for i := 0 to 3 do
@@ -3685,6 +3687,7 @@ begin
FSampleRateOverlay.SetCurrentRate(FController.FSampleRate);
end;
PushRatePresetsToWeb; // тот же источник зеркалим в web-морду
PushBandsToWeb; // band-план бэкенда → web (HF ↔ VHF/UHF)
// Отдельный Pluto RF-AGC блок под AGC: показываем и сдвигаем панели ниже.
LayoutLeftPanel(Caps.HasHWGain);
@@ -5375,6 +5378,27 @@ begin
FWebServer.SetRatePresets(FController.SampleRatePresets);
end;
procedure TMainForm.PushBandsToWeb;
// Band-план бэкенда → web (HF / VHF-UHF). Единый источник — FController.BandPlan,
// тот же BoardUtils-план, что и для desktop-кнопок (RelabelBands).
var
Plan: TBandPlanArray;
Names: array of string;
Freqs: array of Double;
i: Integer;
begin
if FWebServer = nil then Exit;
Plan := FController.BandPlan;
SetLength(Names, Length(Plan));
SetLength(Freqs, Length(Plan));
for i := 0 to High(Plan) do
begin
Names[i] := Plan[i].Name;
Freqs[i] := Plan[i].FreqHz;
end;
FWebServer.SetBands(Names, Freqs);
end;
procedure TMainForm.PushXvtrToWeb;
var
Arr: TWebXvtrArray;
+7
View File
@@ -547,6 +547,8 @@ type
// Единый источник пресетов sample-rate для UI (desktop overlay + web).
// Берётся из caps текущего бэкенда (HPSDR 48k..1536k / Pluto 576k..5760k).
function SampleRatePresets: TBackendRateArray;
// Единый источник band-плана для UI (HF / VHF-UHF по типу бэкенда).
function BandPlan: TBandPlanArray;
// True если активный бэкенд — Pluto (VHF/UHF band-план вместо HF).
function IsPluto: Boolean;
// Человекочитаемое имя подключённой платы для статус-строки (UI/Web).
@@ -3162,6 +3164,11 @@ begin
Result := BackendCaps.RatePresets;
end;
function TRadioController.BandPlan: TBandPlanArray;
begin
Result := BuildBandPlan(IsPluto);
end;
function TRadioController.IsPluto: Boolean;
begin
Result := (FNetwork <> nil) and (FNetwork.Caps.Kind = bkPluto);
+8 -2
View File
@@ -524,8 +524,10 @@ begin
'function updSpan(s){SPANS.forEach((hz,i)=>{const b=q("sb"+i);if(b)b.classList.toggle("on",hz===s);});}' +
'buildSpanButtons();' +
'const BAND_N=["160m","80m","60m","40m","30m","20m","17m","15m","12m","10m","6m"];' +
'const BAND_F=[1900000,3750000,5357000,7100000,10125000,14200000,18120000,21200000,24940000,28500000,50150000];' +
// BAND_N/BAND_F строятся из state.bands (HPSDR HF / Pluto VHF-UHF); ниже —
// bootstrap-значения до первого state. Единый источник — backend в контроллере.
'let BAND_N=["160m","80m","60m","40m","30m","20m","17m","15m","12m","10m","6m"];' +
'let BAND_F=[1840000,3700000,5357000,7100000,10120000,14200000,18130000,21300000,24940000,28400000,50150000];' +
// XVTR-band list, обновляется из state ("xvtr_bands":[{idx,name},...]).
// Активное значение — "xvtr_current" (-1 = HF). В <select> используем
// значения "h<n>" для HF и "x<idx>" для XVTR, чтобы различать.
@@ -689,6 +691,10 @@ begin
// Пресеты sample-rate сменились (другой бэкенд) — перестраиваем span-кнопки.
'if(msg.rate_presets!=null&&Array.isArray(msg.rate_presets)&&msg.rate_presets.length){' +
'if(JSON.stringify(msg.rate_presets)!==JSON.stringify(SPANS)){SPANS=msg.rate_presets;buildSpanButtons();}}' +
// Band-план сменился (другой бэкенд: HF↔VHF/UHF) — перестраиваем band-selector.
'if(msg.bands!=null&&Array.isArray(msg.bands)&&msg.bands.length){' +
'const nn=msg.bands.map(function(b){return b.name;});' +
'if(JSON.stringify(nn)!==JSON.stringify(BAND_N)){BAND_N=nn;BAND_F=msg.bands.map(function(b){return b.freq;});rebuildBandSel();}}' +
// Обновляем localCenter/localSpan из сервера только когда пользователь не тянет
'if(!dragging&&Date.now()-wheelActiveTs>300){' +
// В CTUN без DUP: TX-спектр WDSP центрирован по vfo_a_hz, а не по DDC LO.
+36 -3
View File
@@ -298,6 +298,8 @@ type
FXvtrBands: TWebXvtrArray; // список enabled XVTR (для web-UI)
FDevices: TWebDeviceArray; // saved+discovered устройства (для web-overlay)
FRatePresets: array of Integer; // sample-rate пресеты текущего бэкенда (для span-кнопок)
FBandNames: array of string; // band-план текущего бэкенда (имена кнопок)
FBandFreqs: array of Double; // band-план: дефолтные частоты (для подсветки selector)
FCurrentXvtr: Integer; // -1 = HF, иначе индекс активного XVTR
FOnXvtrBand: TWebCmdXvtrBand;
@@ -394,6 +396,7 @@ type
procedure SetXvtrBands(const ABands: TWebXvtrArray; ACurrent: Integer);
procedure SetDeviceList(const ADevices: TWebDeviceArray);
procedure SetRatePresets(const ARates: array of Integer);
procedure SetBands(const ANames: array of string; const AFreqs: array of Double);
property OnSpan: TWebCmdSpan read FOnSpan write FOnSpan;
property OnVolume: TWebCmdVolume read FOnVolume write FOnVolume;
property OnWfAGC: TWebCmdWfAGC read FOnWfAGC write FOnWfAGC;
@@ -539,6 +542,10 @@ begin
SetLength(FXvtrBands, 0);
// Bootstrap-набор рейтов (до connect): хост перезапишет под бэкенд (HPSDR/Pluto).
SetRatePresets([48000, 96000, 192000, 384000, 768000, 1536000]);
// Bootstrap band-план (HF): хост перезапишет под бэкенд на connect.
SetBands(['160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m'],
[1840000,3700000,5357000,7100000,10120000,14200000,
18130000,21300000,24940000,28400000,50150000]);
end;
procedure TWebServer.SetXvtrBands(const ABands: TWebXvtrArray; ACurrent: Integer);
@@ -583,6 +590,22 @@ begin
end;
end;
procedure TWebServer.SetBands(const ANames: array of string; const AFreqs: array of Double);
// Band-план текущего бэкенда (из TRadioController). Web строит band-selector из
// него — HPSDR (HF) и Pluto (VHF/UHF) имеют разные планы.
var i: Integer;
begin
FStateLock.Enter;
try
SetLength(FBandNames, Length(ANames));
SetLength(FBandFreqs, Length(AFreqs));
for i := 0 to High(ANames) do FBandNames[i] := ANames[i];
for i := 0 to High(AFreqs) do FBandFreqs[i] := AFreqs[i];
finally
FStateLock.Leave;
end;
end;
destructor TWebServer.Destroy;
begin
Stop;
@@ -1561,7 +1584,7 @@ const
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM');
var
FS: TFormatSettings;
XvtrJson, DevJson, RateJson: string;
XvtrJson, DevJson, RateJson, BandJson: string;
i: Integer;
begin
FS := DefaultFormatSettings;
@@ -1576,6 +1599,16 @@ begin
RateJson := RateJson + IntToStr(FRatePresets[i]);
end;
RateJson := RateJson + ']';
// Сборка массива bands: [{"name":"6m","freq":50150000},...] (band-selector web).
BandJson := '[';
for i := 0 to High(FBandNames) do
begin
if i > 0 then BandJson := BandJson + ',';
BandJson := BandJson +
Format('{"name":"%s","freq":%.0f}',
[JsonEscape(FBandNames[i]), FBandFreqs[i]], FS);
end;
BandJson := BandJson + ']';
// Сборка массива xvtr_bands: [{"idx":0,"name":"2m"},...]
XvtrJson := '[';
for i := 0 to High(FXvtrBands) do
@@ -1613,7 +1646,7 @@ begin
'"status_text":"%s","board_text":"%s","ip_text":"%s","supply_text":"%s",' +
'"pll_text":"%s","rx_text":"%s","tx_text":"%s","seq_text":"%s",' +
'"xvtr_current":%d,"xvtr_bands":%s,"freq_mhz_digits":%d,' +
'"fmstep_idx":%d,"devices":%s,"rate_presets":%s}',
'"fmstep_idx":%d,"devices":%s,"rate_presets":%s,"bands":%s}',
[FFreq, FVfoB, FActiveVfo,
FMode, MODE_N[FMode mod 8],
FFilterIdx, FFilterBW,
@@ -1642,7 +1675,7 @@ begin
JsonEscape(FSupplyText), JsonEscape(FPLLText), JsonEscape(FRXText),
JsonEscape(FTXText), JsonEscape(FSeqText),
FCurrentXvtr, XvtrJson, FFreqMhzDigits,
FFMStepIdx, DevJson, RateJson
FFMStepIdx, DevJson, RateJson, BandJson
], FS);
finally
FStateLock.Leave;
+22 -2
View File
@@ -28,7 +28,7 @@ uses
{$IFDEF UNIX}cthreads, BaseUnix,{$ENDIF}
Classes, SysUtils,
RadioController, WebServer, WebAdapter,
HPSDRNetwork, Settings, WDSP, PlatformUtils, DeviceStore;
HPSDRNetwork, Settings, WDSP, PlatformUtils, DeviceStore, BoardUtils;
const
DAEMON_SPEC_WIDTH = 1024; // ширина спектра для StartRunning (web-буфер = 1024)
@@ -56,6 +56,7 @@ type
procedure PushDeviceList;
procedure PushXvtr;
procedure PushRatePresets; // sample-rate пресеты бэкенда → web
procedure PushBands; // band-план бэкенда → web
// Входящие web-команды (из WS-потока): запоминаем параметр и маршалим в Sync*.
procedure WebRun(On_: Boolean);
procedure WebConnect(const IP: string);
@@ -197,7 +198,7 @@ begin
// выходные web-списки, которых нет в периодическом PushState.
case Field of
rfDeviceList: PushDeviceList; // discovered обновился
rfXvtr, rfDevice: begin PushXvtr; PushDeviceList; PushRatePresets; end; // connect / xvtr / backend
rfXvtr, rfDevice: begin PushXvtr; PushDeviceList; PushRatePresets; PushBands; end; // connect / xvtr / backend
end;
end;
@@ -297,6 +298,24 @@ begin
FWeb.SetRatePresets(FCtrl.SampleRatePresets);
end;
procedure THeadlessHost.PushBands;
var
Plan: TBandPlanArray;
Names: array of string;
Freqs: array of Double;
i: Integer;
begin
Plan := FCtrl.BandPlan;
SetLength(Names, Length(Plan));
SetLength(Freqs, Length(Plan));
for i := 0 to High(Plan) do
begin
Names[i] := Plan[i].Name;
Freqs[i] := Plan[i].FreqHz;
end;
FWeb.SetBands(Names, Freqs);
end;
// --- Входящие web-команды (WS-поток) → маршалинг в главный поток ---
procedure THeadlessHost.WebRun(On_: Boolean);
@@ -389,6 +408,7 @@ begin
Log('Web control on http://' + WebCfg.BindAddr + ':' + IntToStr(WebCfg.Port));
PushDeviceList;
PushRatePresets; // дефолтный бэкенд (HPSDR) до connect; перезапишется на смене
PushBands;
// FRunning ставим ДО autostart: первый WDSP-open может занять время, и если за
// него прилетит SIGINT (RequestStop→FRunning:=False), цикл сразу завершится, а