Аттенюатор был только в web и только тремя ступенями (0/-10/-20), а его дБ
никуда не возвращались — при ATT 20 дБ S-метр и спектр уезжали на 20 дБ, и
свежая калибровка уровней была верна лишь при ATT=0.
- RadioController: SetAtten(Db) 0..31 (как умеет железо) вместо индекса *10.
AttenCalDB входит в SMeterCalFor и DispCalFor — стрелка, спектр, водопад и
паны компенсируются, шкала dBm остаётся абсолютной (как Thetis/piHPSDR).
У AD936x аттенюатора нет, там поправка 0 (усиление тракта — hardwaregain).
- Память на диапазон/слот трансвертера по образцу RxGainMode/RxGainDb:
TBandSettings.AttenDB (band 'atten_db') и TXvtrEntry.LastAttenDB
('last_atten_db'); восстановление и отправка в железо — в ApplyBandDSP,
сбор — в MakeBandSettings/SaveCurrentBand, вход в слот — через B.AttenDB.
SetAtten сразу кладёт значение в кэш диапазона/слота (JSON пишется общим
сохранением, а не на каждый шаг ползунка — как у drive).
- MainForm: ряд «ATT [ползунок] NNdB» в RX-блоке левой панели, отступы как у
VOL; на Pluto ряд скрыт и RX-блок ужимается (LayoutLeftPanel двигает
панели ниже). Внешние изменения приходят по rfAtten.
- Web: select → ползунок 0..31 с цифрой, attn_idx -> attn_db, cmd attn {db:}.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WFM использует новые wfmd/wfmmod из нашего форка WDSP. Демодулятор —
квадратурный дискриминатор на dsp_rate, поэтому при штатных 48 кГц
девиация 75 кГц не помещается в полосу. В WFM цепь RXA поднимается до
WFMDSPRate (наибольший FAudioRate*2^k <= 192 кГц, делящий SampleRate
нацело), rsmpin децимирует вход сам. Выше 192 кГц смысла нет: энергия
демодулированного тона выходит на полку (33.5k при 192 кГц против 31.8k
при 576 кГц), а FFT дорожает.
Две ловушки WDSP, найденные по дороге:
* firmin.c: nfor = nc / size, idxmask = nfor - 1. Все fircore создаются
с nc = max(2048, dsp_size), поэтому смена dsp_size без RXASetNC даёт
nfor = 0 — пустой массив планов и падение в fftw_execute.
* fir.c/bandpass.c планируют с FFTW_PATIENT, а wisdom.c пишет в
wdspWisdom00 только степени двойки. При SampleRate 576 кГц наивный
dsp_size = in_size = 12288 требовал FFT на 24576 точек — FFTW мерил
план 25 секунд. Отсюда потолок 192 кГц и dsp_size = степень двойки.
RXA и TXA гейтятся раздельно: рейты TXA (48 кГц вход, 192 кГц выход) от
SampleRate не зависят, поэтому wfmmod уходит на широкий dsp_rate даже
когда приёмник остался на 48 кГц.
Заодно исправлен WDSP_SAM: было 12, а RXA_SAM = 10. Раньше 12 было вне
диапазона и WDSP молча игнорировал (SAM работал как DSB); после появления
RXA_WFM = 12 это включало бы WFM-демодулятор вместо SAM. txaMode
расходится с rxaMode после DRM (WFM: RXA 12, TXA 14) — отсюда ModeToWDSPTX.
DIGI-U/DIGI-L получили свою таблицу фильтров (3.0k…300) и полосу от нуля,
а не от среза на 100 Гц, как у SSB. В CAT они замаплены на коды Kenwood 6
(FSK) и 9 (FSK-R) — раньше эти коды сваливались в USB/LSB.
Новые индексы дописаны в конец (DIGU=8, DIGL=9, WFM=10): индекс режима
уходит в settings.json, band-cache, CAT и web-API. В VfoOverlay пришлось
развести хит-коды — режимы кодировались как -(I+1) и налезли на HIT_NR/
HIT_NB/HIT_SNB, а полосы кодировались значением BW, и WFM-овские
120000/160000/200000 налезли на HIT_AGC_PICK_BASE/HIT_DEV_BASE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Заменил дропдаун+слайдер на одну кнопку RF AGC (с подписью текущего
режима). По клику — оверлей (стиль как FNP-попап) с кнопками
FAST/SLOW/HYBRID/MANUAL + ползунок GAIN 0–73 дБ, активный только в
MANUAL. Крестик/клик мимо закрывают; выбор режима оставляет оверлей
открытым (чтобы сразу подвинуть слайдер). Бэкенд не тронут — те же
команды set_rx_gain_mode/set_rx_gain и state-поля. Только WebPageHtml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Прокинул в web-интерфейс управление, которого там не хватало (раньше
только отображение):
- RF-AGC (Pluto hw-gain): дропдаун режима FAST/SLOW/HYB/MAN + слайдер
GAIN 0–73 дБ (активен только в MAN, драг переводит в manual). Виден
по BackendCaps.HasHWGain (= Pluto подключён). Команды set_rx_gain_mode
/ set_rx_gain → SetRxGainMode / SetRxGain.
- RX MUTE (QO-100 self-monitor): toggle-кнопка рядом с BCN, видна по
InQO100, подсветка по состоянию. Команда set_rx_mute → SetRxMuteOnTx.
Гейтинг зеркалит десктоп. Работает в GUI-web-сервере и headless-демоне.
Проверено на железе пользователем.
WebServer: команды + state-поля (rf_agc_visible/rx_gain_mode/rx_gain_db,
rx_mute_visible/rx_mute_on) + SetRxGainStatus/SetRxMuteStatus.
WebAdapter: колбэки OnRxGain*/OnRxMute (Invoke на поток контроллера) +
публикация в PushState. MainForm + ewsdrd.lpr: привязка колбэков.
WebPageHtml: виджеты, JS-обработчики, updRfAgc/updRxMute, PEND_MAP.
Также актуализирован doc/PLUTO_INTEGRATION_PLAN.md: TX-тракт и режим
QO-100 помечены проверенными на железе; раскрыт TODO «XO ppm-калибровка»
(ручной TxLOOffset как текущий обходной путь, xo_correction/GPSDO как
постоянное решение).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The PLL column (also used for QO-100 beacon status on Pluto) was
minmax(70px,90px), clipping "BCN LOCK 105Hz /7dB". Widen to
minmax(120px,185px), matching the desktop field-7 width. Frontend only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the single tracked-beacon line with a filter band spanning the
beacon width, split into two halves (left/right Manchester lobes at
±400 Hz) with a central divider (carrier null) and band edges at
±450 Hz, matching the desktop dec-marker width. Reference frequency is
now a green dashed line. Frontend only (WebPageHtml).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the desktop QO-100 beacon lock into the web interface and make it
work in the headless daemon.
Backend:
- WebServer: set_beacon/beacon_seed commands (+OnBeacon/OnBeaconSeed
events), beacon state fields, SetBeaconStatus, and beacon_visible/on/
text/ref_hz/track_hz in BuildStateJson. beacon_seed carries 'frac'
(0..1 click position); absolute Hz is computed controller-side from
live FCenterFreq/FSpanHz (mirrors desktop PixelToFreq).
- WebAdapter: OnBeacon->SetBeaconLock, OnBeaconSeed->BeaconSeedAtHz;
PushState mirrors compact status (matches MainForm.BeaconStatusText)
into the PLL status cell when visible (Pluto + active XVTR).
- MainForm/ewsdrd: wire the two callbacks.
- ewsdrd: call ServiceBeaconLock in the main loop @~10Hz (FRunning+
FWDSPReady gated). Without this the lock loop never ran headless.
Frontend (WebPageHtml):
- BCN button (visible only on Pluto+XVTR), highlighted while locked.
- Seed-on-mousedown with immediate return (like desktop FormMouseDown):
no drag/set_center so the gesture never moves the center / tears IQ.
Armed (first click after BCN) or Shift, mirroring desktop arm/Shift.
- PLL status cell shows beacon status (field-7 parity with desktop).
- Ref (green) + tracked (orange) beacon markers on spectrum/waterfall.
Known issue (unresolved): after lock the beacon can slowly drift off and
drop to "BCN sync". Suspected residual-sign anti-drift or retune-timing
on the web/daemon path; needs on-air diagnostics. See memory
project_web_beacon for the investigation state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix: dragging/wheeling the spectrum in the web UI clamped frequency to a
hardcoded HF ceiling (vfoMax=60 MHz), so on Pluto VHF/UHF it snapped down
to 60 MHz and couldn't pan up (only the VFO wheel worked, via a different
limit). Push the backend's tuning range (BackendCaps.MinFreqHz/MaxFreqHz)
to the web alongside the band plan; JS vfoMin/vfoMax use freq_min/freq_max
from state. The transverter branch (XVTR_CUR>=0) is unchanged — it keeps
the wide-open range since the displayed RF freq is translated to IF and
clamped server-side.
Dead-code cleanup (audit of this session's work):
- SampleRateMode/TSampleRateMode: orphaned when the sample-rate
unification replaced the srmContinuous branch in ApplyBackendCapsToUI;
no readers left. Removed (+ now-unused PLUTO_MAX_SR).
- Pre-existing write-only/unused: TBackendCaps.MaxSampleRate,
SampleRateOverlay.SPAN_NAMES, MainForm.BAND_FREQ.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
Move discovery into the controller and add a full web device overlay
(discovery + connect + saved-device CRUD), reaching parity with the
desktop device dialog.
- RadioController: OnDeviceFound/Discover/DiscoverFinishedNone +
rfDeviceList; TDiscoverThread moved in from MainForm.
- WebServer: device protocol (discover/connect/disconnect/dev_add/
dev_remove/dev_autostart) + "devices" in BuildStateJson.
- MainForm: web handlers (marshalled), PushDeviceListToWeb, extracted
DoStopAndDisconnect, detached WebOnRun from BtnStartStopClick;
web connect resolves board type from discovered then saved by IP.
- DeviceForm: RefreshFound renders the found list from the store.
- WebPageHtml: DEV overlay; START opens it, STOP disconnects.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The outbound channel already existed: PushLoop broadcasts the full
BuildStateJson (including "mute") to web clients every 200ms. The gap was
purely client-side — applyState never rendered the mute icon, so a
UI-initiated mute change was not reflected in the browser (the icon was
only updated on the client's own click).
WebPageHtml.pas:
- Add updMuteIc() helper; render it from applyState (guarded by
isPending("mute")), so periodic state updates the icon.
- Click handler calls updMuteIc + pend("mute"); add "mute" to PEND_MAP so
the optimistic local state isn't clobbered by an in-flight stale push.
Mute is now bidirectional. Other fields already render in applyState, so
UI->web works for them; this confirms the web outbound path is the
periodic full-state broadcast, not a per-field event push (the latter
will matter for the future serial/Arduino frontend).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WebPageHtml.pas:
- Add MIC gain slider (0..300%) with localStorage persistence; inserts
Web Audio GainNode between MediaStreamSource and Opus encoder.
- Disable autoGainControl in getUserMedia so the manual gain actually works.
- Pre-create+resume AudioContext synchronously inside the MOX click handler
(before any await) so user-gesture context isn't lost during getUserMedia
on macOS/Android.
- Call startTXMic() from the click handler too — the call from applyState
runs in a WS-message context which mobile browsers reject.
- Re-resume txMicACtx on every MOX→TX (Android can suspend it between
cycles) and reuse the existing context instead of recreating per session.
- stopTXMic now a no-op: the previous flag reset created a race that spun
up duplicate pipelines on rapid RX→TX toggles.
WebServer.pas:
- Pre-roll cushion: hold first 40ms of decoded mic samples before flushing
to FTXMicRing, raising steady-state ring depth from ~9ms to ~50ms so
WiFi jitter no longer drains the buffer into silence ("robotic" voice).
- Wall-clock-based Opus PLC: synthesize up to 5 frames via
opus_decode_float(NULL) when packet gap exceeds expected 20ms cadence.
- A gap > 200ms is treated as a new TX session: pre-roll resets, PLC
is skipped (no useful decoder state).
- Local (non-web) mic paths are untouched — they bypass WebOnMic entirely.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Web UI: stepSel.onchange now sends fm_step command to server when in FM
mode; option values stay in Hz so tuneStep works unchanged.
WebServer: add fm_step command handler and OnFMStep callback.
MainForm: wire WebOnFMStep -> SyncWebFMStep -> SetFMStep + SaveCurrentBand.
Web spectrum/ruler: vertical FM-step grid lines in drawSpec; FM-aligned
ticks and labels in drawRuler with auto label-density (short ticks between
labeled lines, measureText-based labelMult to prevent overlap).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add LastCTun, LastAGCMode, LastAGCTop to TXvtrEntry; SaveCurrentBand
now captures all three in the XVTR branch; ActivateXvtrBand restores
them on slot switch/startup (previously CTun was always reset to False
and AGC was never saved/restored for XVTR)
- WebServer: expose fmstep_idx in state JSON so web clients sync the
FM step from desktop state
- WebPageHtml: swap stepSel options to FM steps (6.25k/12.5k/20k/25k)
when mode=FM, restore HF steps when leaving FM; syncs fmstep_idx
from server on connect and mode change
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix web spectrum scroll resetting freq to XVTR minimum (same root
cause as desktop: hardcoded 60 MHz clamp; now delegates to server)
- Save/restore FM mode, NFM/WFM filter, squelch, CTCSS per XVTR slot
and per HF band (CTCSSOn/ToneIdx added to TBandSettings; full state
added to TXvtrEntry; ActivateXvtrBand restores all on slot switch)
- Fix UpdateFilterButtons always resetting NFM/WFM to default on
mode restore; now preserves saved filter index
- Add FM Step tuning: panel with STEP toggle + 6.25/12.5/20/25 kHz
selector (popup); mouse wheel uses selected step in FM mode;
state saved per band and XVTR slot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LSB/USB: 5.0k–1.0k (unchanged)
- DSB: split into own group 16k–2.4k (was sharing AM filters)
- CWL/CWU: 1.0k 800 600 500 400 250 150 100 50 25 (removed 750, added 150)
- AM/SAM: 20k–5.0k (new wider range)
- Web FNP FM: now shows NFM/FM only (11k/16k), matching desktop
- Fix SyncWebFilter: FM array out-of-bounds; FFMDeviation now set on web filter select
- VfoOverlay: DSB separated from SSB with own quick-select values
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Power bar: minor ticks every 5% (between 0/25/50/75/100% main marks)
SWR bar: minor ticks at integer values 2,3,4,6,7,8,9 (between 1/2.5/5/10)
Applied to both desktop (SpectrumView) and web (paintTXMeter JS)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WebServer: add fwd_w, swr, pa_max_power to state JSON and PushSpectrum
- MainForm: pass FLastFwdW, FLastSWR, FPAMaxPower to PushSpectrum
- Web UI: paintSM switches to paintTXMeter when transmitting/tuning;
same layout as desktop — two sub-bars at 44-58% height, power ticks
above, SWR ticks below, SWR>2.5 turns red with "SWR High" label
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Settings > Display: new "VFO Display" control to select max frequency
range (999 MHz / 9.999 GHz / 99.999 GHz), persisted as freq_mhz_digits
- FreqDisplay: extend digit map [0..10] to support up to 11 digit positions;
keyboard navigation adapts to MinMhzDigits dynamically
- MainForm/LayoutTopVfoBlock: VFO panel width scales automatically with
selected digit count using dynamic text measurement
- Web VFO: fix broken HTML (class attr not closed before data-s — wheel and
hover were completely non-functional); rewrite fmtVfo to use variable MHz
digit count synced from server state; fix missing </span> for .vfo-hz
- WebServer: publish freq_mhz_digits in state JSON; updated from
ApplyFreqMhzDigits so desktop and web stay in sync
- Fix Int32 overflow on frequencies above ~2.15 GHz: CATEngine.Pad V param
Int64, FormatFreq/FormatFreqSV Mhz var Int64 (Mhz*1000000 overflowed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Browser: AudioWorklet accumulates 960-sample frames, AudioEncoder encodes
to Opus, sends binary WS frame 'M' + raw Opus packet
- Server: opcode $02 handler decodes Opus via libopus, calls OnWebMic callback
- MainForm: WebOnMic converts PSingle→Double, pushes to PushTXMicSamplesD;
ApplyMOX switches mic source to txmsWeb when web client active, restores on TX-off
- WDSPEngine: added txmsWeb to TTXMicSource enum
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deadlock: BroadcastBinary held FClientLock during blocking SockSend;
slow/stalled client caused Stop() to wait forever. Fix: set SO_SNDTIMEO=1s
on each accepted socket so SockSend returns error instead of hanging,
mark client wsClosed on send failure so subsequent ticks skip it.
Mobile: move TONE/TX-slider/MOX to front of toolbar via CSS order so
they are immediately visible without horizontal scroll on phones.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Change filter passband fill from blue/white to semi-transparent red
on both spectrum and waterfall when transmitting or tuning, matching
the desktop behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add 'dup' field to state JSON (FDisplayDuplex). In CTUN+non-DUP TX,
use vfo_a_hz as spectrum center so the TX signal renders at the VFO
position instead of the display center. In CTUN+DUP TX, keep center_hz
so the RX spectrum stays correctly positioned at the filter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Track wheelActiveTs on each wheel tick; suppress server center_hz
overwrites for 300ms after last wheel event so localCenter stays at
the current VFO position instead of snapping back to screen center.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Click on spectrum/waterfall no longer sets localCenter immediately.
The filter passband now appears exactly at the clicked pixel instead of
jumping to screen center. Server's center_hz (arrives ~200ms later)
naturally re-centers the display once fresh spectrum data is ready.
VFO display wheel (vDispA/vDispB) now updates localCenter when tuning
the active VFO. Previously localCenter lagged behind, causing applyState
to teleport the filter back to center every 200ms (the "passes through
center" oscillation).
Spectrum canvas wheel (doWheel) is unchanged: user expects the spectrum
to scroll when wheel-scrolling over it, filter stays at center — correct.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add freq_a command: sets VFO A regardless of active VFO; fixes A=B and
SWAP when VFO B is active (prev. sent freq_b which overwrote wrong VFO)
- Add pend("freq") to B=A / A=B / SWAP to guard local display during round-trip
- RX step attenuator: HPSDRNetwork.SetStepAtten → bytes 1442/1443 in HP packet;
WebServer attn command; MainForm FAtten + SyncWebAttn; attSel.onchange in JS
- TUN mode: set_tun WebSocket command wired to ApplyTUN(); toneBtn toggles
with on/off highlight and pending protection; tuning field in state JSON
- FNP button opens semi-transparent filter overlay (10 buttons, 5×2 grid)
with mode-appropriate widths (SSB/CW/AM/FM); active filter highlighted;
cur.filter_bw updated immediately on click so passband redraws within 50ms
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>