feat: unify sample-rate presets across desktop + web UIs

Sample-rate (span) presets were duplicated in three places and none was
authoritative: HPSDR set hardcoded in the desktop overlay (SPAN_RATES),
Pluto set in backend caps, and a separate hardcoded list in the web JS.
The web showed HPSDR rates even on Pluto, and the web adapter silently
dropped any rate outside a hardcoded HPSDR whitelist (so Pluto-only rates
like 960k/2304k never switched).

Single source of truth = backend caps -> controller:
- HPSDRNetwork.Caps now fills RatePresets [48k..1536k] (was nil).
- RadioBackend: named type TBackendRateArray.
- TRadioController.SampleRatePresets returns BackendCaps.RatePresets.

Both UIs derive from it:
- Desktop: ApplyBackendCapsToUI always feeds the overlay from
  SampleRatePresets (overlay no longer owns the HPSDR list).
- Web: WebServer.SetRatePresets + rate_presets in state JSON; hosts
  (daemon OnState/startup, GUI ApplyBackendCapsToUI/startup) push
  SampleRatePresets; JS builds span buttons dynamically from it.
- WebAdapter.SyncSpan validates against SampleRatePresets instead of a
  hardcoded whitelist, so any backend rate is accepted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 13:52:03 +03:00
co-authored by Claude Opus 4.8
parent d78d835485
commit 6b695ce756
8 changed files with 87 additions and 21 deletions
+10 -5
View File
@@ -16,7 +16,7 @@ unit WebPageHtml;
- Opus: decodeFrame() правильный метод, async/await
- Opus: opQueue буферизует пакеты пока WASM не готов
- Opus: AudioContext создаётся только по user gesture (оверлей)
- SPANS: только 48K..1536K
- SPANS: строятся из state.rate_presets (HPSDR/Pluto разные наборы)
- (Mode fix) modTglBtn теперь отображает ТЕКУЩИЙ активный режим
(LSB/USB, CWL/CWU, AM/SAM, FM), а не следующий. При клике
переключает на парный режим внутри группы.
@@ -516,11 +516,13 @@ begin
'q("btnSwap").onclick=()=>{const t=vfoA;vfoA=vfoB;vfoB=t;renderVfos();pend("freq");' +
'ws2({cmd:"freq_a",hz:Math.round(vfoA)});ws2({cmd:"freq_b",hz:Math.round(vfoB)});};' +
// SPAN кнопки: только 48K..1536K
'const SPANS=[48000,96000,192000,384000,768000,1536000];' +
'const SPAN_L=["48K","96K","192K","384K","768K","1536K"];' +
'(function(){const c=q("spanOv");SPANS.forEach((hz,i)=>{const b=document.createElement("button");b.className="sp-btn";b.id="sb"+i;b.textContent=SPAN_L[i];b.onclick=()=>ws2({cmd:"span",hz});c.appendChild(b);});})();' +
// SPAN/sample-rate кнопки: строятся из state.rate_presets (HPSDR и Pluto
// имеют разные наборы; единый источник — backend caps в контроллере).
'let SPANS=[48000,96000,192000,384000,768000,1536000];' +
'function fmtRate(hz){return Math.round(hz/1000)+"K";}' +
'function buildSpanButtons(){const c=q("spanOv");if(!c)return;c.innerHTML="";SPANS.forEach((hz,i)=>{const b=document.createElement("button");b.className="sp-btn";b.id="sb"+i;b.textContent=fmtRate(hz);b.onclick=()=>ws2({cmd:"span",hz});c.appendChild(b);});updSpan(localSpan);}' +
'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];' +
@@ -684,6 +686,9 @@ begin
'if(bandsChanged)rebuildBandSel();' +
'else if(curChanged){const s=q("bandSel");if(s&&XVTR_CUR>=0)s.value="x"+XVTR_CUR;}' +
'}' +
// Пресеты 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();}}' +
// Обновляем localCenter/localSpan из сервера только когда пользователь не тянет
'if(!dragging&&Date.now()-wheelActiveTs>300){' +
// В CTUN без DUP: TX-спектр WDSP центрирован по vfo_a_hz, а не по DDC LO.