Independent RX slices sharing one panadapter: each has its own
freq/mode/filter/AGC/NR-NB-SNB-ANF, S-meter, volume and sound card.
VFO A = main receiver; B+ are soft fan-out WDSP channels within the
captured span (ideal for Pluto/QO-100; HW DDC path is Stage 3).
- WDSPEngine: soft fan-out (TSliceChan, ProcessSlices in DSP thread,
per-slice NCO/mode/filter/AGC/NR/NB/SNB/ANF + S-meter, OnSliceAudio),
FSliceLock serialises add/remove/set vs DSP; reopened on rate change.
- RadioController: TCtrlSlice with its own TAudioOutput/device,
Add/Remove/SetSlice*, RefreshSliceShifts keeps slices on target Hz
across centre changes. Slice letters start at 'B' (A = main receiver).
- UI: per-slice flags (VfoOverlay) with letter badge/close, filter-band
markers in BOTH GL and CPU renderers, drag-retune, wheel; flags placed
beside the filter band with flip, like the main flag. Ctrl+click adds.
- Main flag (slice A) visible whenever the left panel is hidden OR any
slice exists; synced with panel-driven DSP/mode/AGC changes.
- Perf: S-meter decoupled from flag chrome cache (FMeterDirty +
meter-only RefreshMeter via EnsureRendered); InvalidateVfoOverlay
avoids global overlay-cache churn on meter ticks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TX-индикатор теперь кликабельный (хит HIT_TX → колбэк OnTxSelect) — задел
под выбор TX-слайса в мультислайсах. Три состояния цвета:
- передача идёт → красный;
- слайс выбран для TX → янтарный (CLR_TX_SEL);
- не выбран → нейтральный.
Новое состояние FTxSel прокидывается через SetExtState (пока True — один
слайс всегда TX-слайс). MainForm.OnVfoOverlayTxSelect — заглушка с TODO под
мультислайсы.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PositionVfoOverlay считает обе кромки полосы фильтра и ставит накладку справа
от неё по умолчанию; если справа не влезает (маркер у правого края) — рисует
слева от левой кромки. Возврат на правую сторону автоматически, когда снова
помещается. Убран прежний clamp-оверлап с маркером.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Полный редизайн накладки поверх спектра: шапка (слайс-бейдж A/B, ширина
фильтра, кликабельный SPLIT, индикатор TX), метр-ряд (индикаторы NR/NB/ANF/
SNB + сигнал-бар), крупная частота + S, нижний бар с fly-out пикерами.
- Fly-out движок MODE/DSP/AUD (раскрываются под баром, карточка растёт):
MODE — сетка режимов 4×2 + фильтры; DSP — NR/NB/SNB/ANF + AGC; AUD —
выбор аудио-устройства вывода (задел под слайсы).
- Слайдер громкости (click+drag) + mute; SPLIT-toggle; live TX-индикатор.
- Все контролы подключены к RadioController (SetVolume/ToggleMute/SetSplit/
EnumOutputDevices→ApplyAudioDevice); wire в MainForm + HandleMouseUp, чтобы
клик по накладке не уходил в спектр.
- FormatFreq на Int64 (double накапливал ошибку в десятки Гц на ~10 ГГц QO-100).
- S-метр ниже по высоте + плавный градиент по уровню dBm: тёмно-зелёный→
зелёный→жёлтый→красный (красный к ~S9+18).
- Кэш+dirty рендер сохранён (CPU-паритет со старой накладкой), GL-путь через
DrawOverlayBitmap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bidirectional Apache Labs Andromeda/G2 front-panel protocol on top of CAT.
Panel logic lives in dedicated units; existing files get only thin hooks.
New units:
- AndromedaMap.pas — data only: action enums + default button/encoder
layout tables and LED (ZZZI) numbers (mirroring the Thetis defaults)
- AndromedaPanel.pas — TAndromedaPanel: inbound panel events -> controller
actions (marshalled via Invoke), state subscription -> ZZZI push via SendProc
Hooks:
- CATEngine: parse ZZZD/ZZZU/ZZZE/ZZZP/ZZZS into generic TCATContext callbacks
- CATAdapter: creates the panel, routes engine callbacks to it, supplies the
SendProc to the Andromeda port, AddStateListener, ZZZS handshake
- CATSerial: Andromeda flag in port config + HasAndromeda/SendToAndromeda
- RadioController: AddStateListener (multicast, does not steal MainForm's
OnStateChanged)
- Settings/SettingsForm/MainForm: CATSerialAndromeda[0..3] + persistence + checkbox
First version: serial transport, hardcoded default mapping, LED indicators
(MOX/Tune/CTUN/VFO A-B/Split/NB/NR/SNB/ANF/Squelch). Doc: doc/ANDROMEDA.md.
TODO: default button mapping does not match the physical panel (hardcoded) —
to be fixed later; also ZZMF/LCD text, TCP G2V2 transport, configurable mapping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SyncWebConnect (web-коннект на десктопной GUI) собирал устройство сам
только из IP+BoardType, оставляя Kind=0 (=bkHPSDR) и пустые URI/Serial.
Из-за этого сохранённый Pluto подключался как openHPSDR → UDP discovery
→ Hardware Timeout (web при этом оптимистично показывал connected /
Unknown Board). Десктоп-путь и демон уже шли через ResolveDevice.
Теперь SyncWebConnect тоже зовёт FController.ResolveDevice(IP) —
единый резолв (восстанавливает Kind/URI/Serial/BoardType/MAC из стора
по IP или URI) для всех трёх входов.
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>
Add a thin colored strip along the bottom of the spectrum (above the
ruler) showing the QO-100 narrow-band transponder bandplan: mode
segments (CW ONLY / NB DIGI / DIGI / SSB / MIXED MODES) plus the three
beacons (CW / PSK / EXP) as muted-red cells at their ranges.
- New unit BandPlanOverlay.pas (TBandPlanOverlay), modelled on
VfoOverlay: content rendered once into a cache bitmap, per-frame cost
is just a constant-alpha composite (CPU) / one textured quad (GL).
Cache invalidates only on view change (center/span/width) so it idles
while the QO-100 downlink display is static.
- Wired into both render paths: SpectrumView (CPU composite) and
SpectrumViewOpengl (texture, re-uploaded only when dirty).
- MainForm: SetView fed from SyncSpecViewFreq; visibility gated by
InQO100 (Pluto + full-duplex transponder), like RX MUTE / BEACON.
- DPI-robust: strip height scales with Screen.PixelsPerInch and the font
height is tied to the strip height (not point size), so labels always
fit at any Windows scaling.
- Segment/beacon frequencies and colors are a single editable table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a per-operator toggle to hear your own downlink off the satellite
while transmitting (full-duplex self-monitor), and restrict QO-100-only
controls to the QO-100 mode.
- WDSPEngine: new MonitorRXDuringTX flag; audio gate becomes
`not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX)` so RX audio
can pass to speakers during TX in full-duplex.
- Controller: FRxMuteOnTx (default True = mute RX on TX), SetRxMuteOnTx/
ApplyRxMuteToEngine, rfRxMuteOnTx notify; new InQO100 predicate
(Pluto + full-duplex XVTR slot) as the single gate for QO-100 UI.
- Settings: global RxMuteOnTx (rx_mute_on_tx, default True), persisted.
- MainForm: new RX MUTE button in TX panel next to DUP, visible only in
QO-100; BEACON button visibility tightened from "any transverter" to
InQO100 so it no longer shows in plain VHF/UHF modes.
- doc: plan updated (RF-AGC done, two temps, TX/RX locked split,
self-monitor, phase 6 done; remaining TODOs incl. web wiring).
Web intentionally left unchanged for now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pluto exposes two temperature sensors: the AD9361 transceiver
(ad9361-phy temp0/input, milli-°C) and the Zynq SoC/FPGA (xadc temp0,
raw+offset+scale → C = (raw+offset)*scale/1000). Only the former was
read. Now both are read and shown as "Temp 37/53C" (chip/SoC) in the
desktop status bar and web UI.
- ReadTelemetry gains an out SocTempC param (base + Pluto override);
Pluto reads xadc via the IIO context.
- Controller stores FLastPlutoSocTempC; SoC temp validated with a
plausibility window (no slew-limit — stable local sensor, unlike the
AD9361 sensor that spikes during mid-conversion).
- Status formatting handles one or both temps in MainForm and WebAdapter.
- Widen desktop status-bar field 3 (150→190) so both temps + RSSI fit;
web layout unchanged.
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>
The headless daemon (ewsdrd) only ever built HPSDR devices, so Pluto
could not be used: backend is chosen by Dev.Kind and Pluto opens by URI,
neither of which the daemon propagated. Also local audio played on the
host and web Discover never probed network Plutos.
Shared, backend-agnostic helpers in TRadioController (used by GUI + daemon):
- ResolveDevice(IP): discovered->saved lookup restoring Kind/URI/Serial/
BoardType/MAC. MainForm.ResolveDevice now delegates here.
- AddSavedDevice(name, addr): web dev_add saves Pluto when addr has a URI
scheme (ip:/usb:/local:), else HPSDR. Both web hosts use it.
- SeedPlutoProbeFromSaved: seed network-probe URIs from saved Plutos
(no mDNS -> a network Pluto is only found by direct URI probe).
- LocalAudioEnabled flag (GUI=True): when False the local sound card is
neither opened nor written; RX audio goes only to web (OnAudioConsume).
DeviceStore.AutoStartDevice(out Dev): full autostart record (Kind/URI/
Serial) so a saved Pluto (URI-addressed, empty IPAddress on USB) can
autostart.
Daemon (ewsdrd.lpr): LocalAudioEnabled:=False; SyncConnect via
ResolveDevice; autostart via AutoStartDevice; SyncDiscover seeds probe
URIs then Discover; SyncDevAdd via AddSavedDevice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the display-FFT centroid beacon lock with a decoder-based loop.
The beacon decoder already tracks the carrier precisely via its NCO
(squaring-FFT acquisition + Costas offload); its residual ResidHz+CarFreqHz
is an exact measurement of the beacon's offset from the aim point. Feed that
into LOError of the active transverter so the LO retunes to compensate LNB
drift — the whole downlink stays put, calibration persists.
RadioController:
- Remove MeasureBeaconFFT and all display-FFT lock state/constants
(lobe/search/track/slew/sym/shape/seed-gain/prom), fields
FBeaconManualSeed/FBeaconPromDB/FBeaconDecOn, methods
SetBeaconDecode/SetBeaconDecodeAtHz/BeaconDecodeEnabled.
- SetBeaconLock is now the master switch (lock = decoder); BeaconSeedAtHz
aims the decoder; ServiceBeaconLock measures beacon freq from the decoder
scope and trims LOError (gain 0.5, deadband 20 Hz, step clamp 400 Hz,
settle 8 ticks). Feed-forward of the aim keeps the decoder locked through
the retune (net baseband ~0). Lock/SNR now come from the decoder.
MainForm: single BEACON button — left-click toggles lock (+ opens the
constellation/bulletin scope, arms the spectrum click for aiming),
right-click shows/hides the scope. Spectrum click (armed or Shift) aims via
BeaconSeedAtHz. Simplified status field and markers.
Both targets build (lazbuild --ws=qt6 + build-ewsdrd.sh). Not yet verified
on air. Risk: BEACON_RESID_SIGN=+1.0 — flip to -1.0 if the loop runs away.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The DUP toggle wrote FDisplayDuplex directly, bypassing the controller's
SetDuplex (which also sets KeepRXDuringTX live and fires rfDuplex). And
MainForm had no rfDuplex render case, so QO-100 auto-enabling duplex
(ActivateXvtr → SetDuplex) left the button unlit while FDisplayDuplex=True
— pressing DUP then turned it OFF when the user expected ON.
Single path now: ApplyDUP just calls SetDuplex; a new rfDuplex render case
styles the button and (when transmitting) switches the display source.
Both the button and QO-100 auto-enable go through it, so the button always
reflects state and KeepRXDuringTX stays in sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
QO-100 reuses the transverter path (VFO/band-stack/beacon-lock untouched)
with a split LO for the geostationary transponder:
- TXvtrEntry += FullDuplexTx + TxLOOffset (persisted). Reserve the last
XVTR slot (QO100_SLOT) for QO-100; migration restores the template when
the slot is unset or has empty offsets (fixes old configs showing zeros).
- XvtrTranslateTX: TX LO = visible - TxLOOffset (no LNB LOError) for QO-100;
identical to RX translate for normal transverters. Switched all TX-side
translate calls (controller + MainForm).
- SetDuplex wired (live KeepRXDuringTX); auto display-duplex on QO-100 entry.
- New "QO-100" settings page (LNB LO / transponder offset / downlink edges /
RX gain / TX ceiling + live RX-IF/TX readout); QO slot hidden from the
transverter table. Reuses OnXvtrChange — no MainForm wiring needed.
- doc/PLUTO_INTEGRATION_PLAN.md: phase 5 done.
Builds clean (headless + qt6 GUI). Not yet verified on hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Show current drive value next to the slider: percent for openHPSDR,
TX attenuation in dB for Pluto (via controller PlutoTxAttForDrive;
unit flips on backend connect). Refreshed on drive change, device
render, and TX max-att change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Status bar / web now show Pluto/LibreSDR model + serial instead of
"Unknown Board" (BoardDisplayName in controller; UI just renders it).
- Field 3 shows AD936x chip temperature + RX RSSI for Pluto (no PA →
Supply only for openHPSDR). Polled via backend.ReadTelemetry (~2 Hz),
filtered for sensor glitches (plausibility window + slew-limit, with
anti-stick) — fixes rare bogus -15°C readings.
- Add libiio binding iio_channel_attr_read_double.
- Remove stale root pluto_integration.MD (superseded by
doc/PLUTO_INTEGRATION_PLAN.md).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds on the beacon-lock baseline: keeps the proven display-FFT control
path (hardware-LO trim folded into the active transverter LOError), and
hardens it. The narrowband NCO/FLL scaffold (BeaconLock.pas) is removed —
it was never in the control path.
- RadioController:
* Persist live: each correction folds straight into the active xvtr
LOError (visible in Settings, survives restart as LNB calibration);
disk writes throttled. FBeaconLockHz is now only a session indicator.
* Robust tracking vs nearby signals: narrow ±2 kHz gate while locked
(±6 kHz only for acquisition), slew-rate outlier rejection (a sudden
centroid jump = interferer, not slow LNB drift → ignored), and lobe
shape validation in MeasureBeaconFFT (width + power symmetry) so a CW
carrier / one-sided neighbour isn't mistaken for the BPSK beacon.
Prominence measured over the in-window noise floor (gate-width robust).
* Fast convergence: feed-forward (predict post-retune position so the
window follows the beacon through the jump) + near-deadbeat correction
on the manual click → one click locks instead of repeated tapping.
* Single central BPSK beacon (10489.750); dead CoarseBeaconOffset and
unused throttle counters removed; stale NCO/FLL/4 Hz comments fixed.
- WDSPEngine: drop the per-sample TBeaconTracker RX-IQ tap (tracker gone).
- MainForm: BEACON is now a plain button, shown only on Pluto in a
transverter. Lock status (off / click / search / LOCK + correction +
prominence) moves to status-bar field 7 in place of PLL on Pluto;
openHPSDR keeps PLL there.
- SettingsForm/StatusBar: widen LO Error field to ±10 MHz (QO-100 LNBs
drift hundreds of kHz) and widen status field 7 for the beacon text.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Click the beacon on the spectrum; the controller tracks it and continuously
trims the transverter LO so the whole downlink stays put as the LNB drifts.
- BeaconLock.pas: TBeaconTracker scaffold (narrowband NCO/FLL) — kept but no
longer in the control path (FLL diverged on the suppressed-carrier BPSK
beacon + Pluto DC offset). Measurement now uses the proven display-FFT path.
- RadioController: FBeaconLockHz folded into XvtrTranslate (on top of LOError).
MeasureBeaconFFT finds the beacon by POWER-WEIGHTED CENTROID of the lobe
(BPSK has a suppressed carrier → broad symmetric ~2.4 kHz lobe, not a line;
a peak-finder jitters, the centroid sits on the carrier). Track position in
ABSOLUTE display Hz (FBeaconTrackHz) so scrolling the waterfall/centre never
moves the search window off the beacon. Closed loop: e = tracked - ref,
d(e)/d(LockHz) = -1 -> LockHz += gain*e; settle cooldown after each correction
(display-FFT lags the LO retune -> avoids overshoot); deadband 75 Hz +
centroid/prominence smoothing + lock hysteresis for a steady lock.
Correction applied only after the user clicks (manual seed), so a crowded
band can't pull a neighbour to the reference.
- WDSPEngine: SetBeaconTracker + RX-IQ tap (tracker idle now, ~1 check/sample).
- SpectrumView + GL: beacon markers — green = reference (10489.750), orange =
tracked centroid.
- MainForm: BEACON button (RX block, XVTR-only). Flow: BEACON -> "CLICK BCN"
-> click beacon -> "L<corr> /<prom>". ServiceBeaconLock driven by MeterTimer.
Reference defaults to the middle BPSK beacon 10489.750. Tested on-air against
Es'hail-2: locks and holds, survives waterfall drag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CTun position was lost on band restore / restart, and VHF/UHF bands were
never saved on Pluto.
- SaveCurrentBand: the >61 MHz guard (HPSDR HF-only) skipped saving native
Pluto VHF/UHF bands → 2m/70cm reverted to defaults. Now HPSDR-only; Pluto
saves VU-plan bands (FreqToBandIdx guards cross-band pollution).
- Persist DDC center for CTun: new TBandSettings.CenterHz (+ JSON center_hz)
and TXvtrEntry.LastCenterHz (+ last_center_hz). RestoreBand / ActivateXvtr
restore the saved center when CTun is on and the offset fits the span,
instead of forcing center := VFO (which re-centered the receiver).
- UpdateFreqDisplayMax: freq-display upper bound from active transverter's
FreqEnd (lets QO-100 show 10 GHz past the 6 GHz Pluto cap).
- QO-100 transverter template (slot 2, disabled): downlink 10489.5–10490.0,
LO 9750. MigrateXvtrTemplate injects it into the first free slot for
devices that already have a saved xvtr config (template was otherwise
masked by the loaded slots).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- RX hw-gain (Pluto/AD936x): backend SetRxGain(ModeIdx, GainDb) — gain_control_mode
(manual/fast_attack/slow_attack/hybrid) + hardwaregain. Controller FRxGainMode/
FRxGainDb, commands SetRxGainMode/SetRxGain/RxGainBy, rfRxGain event, snapshot,
applied in StartRunning. Persisted per-device (rx_gain_mode/rx_gain_db). HPSDR
unaffected (backend no-op).
- UI: separate "RF AGC" panel below the (unchanged) WDSP AGC block, shown only for
Pluto — FAST/SLOW/HYB/MAN mode buttons + manual GAIN slider. LayoutLeftPanel
reflows lower panels via RelayoutBelowBands; no layout shift on HPSDR.
- Cold-init fix: direct sampling_frequency write doesn't load the AD9361 FIR
decimation filter, so a cold-booted Pluto showed a wide centre spike that didn't
track tuning (worked only after SDR Console pre-initialised it). Add optional
libad9361 binding (dynamic) and call ad9361_set_bb_rate() in ApplySampleRate,
which configures BBPLL + loads the FIR; falls back to direct write if absent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ADALM-Pluto / AD9361 support as a second hardware backend alongside
openHPSDR, sharing the WDSP DSP pipeline and the existing controller API
(UI stays decoupled from logic).
- RadioBackend.pas: abstract TRadioBackend + TBackendCaps + TRadioDevice
(Kind/URI/Serial). THPSDRNetwork now derives from it (state via virtual
getters); TRadioController.FNetwork is the base type.
- IIOBindings.pas: dynamic libiio loader (runs without libiio present).
- PlutoBackend.pas: scan/probe-by-URI, connect, LO/rate/bandwidth/gain
control, RX streaming thread (int16->24bit BE -> OnDDCIQ), Q conjugated
to match WDSP IQ convention. Verified on LibreSDR (AD9361) over network.
- Unified discovery: TDiscoverThread scans both backends; network Plutos
found via direct ProbeURI (no mDNS needed). ConnectDevice dispatches by
Dev.Kind (EnsureBackend swaps backend, preserving callbacks).
- DeviceStore/DeviceForm: persist Kind/URI/Serial; save Pluto via
AddSavedPluto so saved/autostart devices reconnect across restarts.
- VHF/UHF band plan (BoardUtils, kind-aware): 6m..ADS-B for Pluto; fixes
HF clamps (band detect, mouse-wheel 60 MHz cap, freq-display max).
- SampleRateOverlay: configurable presets (Pluto 576k..5760k, >520 ksps),
auto-width to fit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ConnectAndRun (ConnectDevice + RestoreBand + XVTR restore + active-VFO retune +
StartRunning) and StopAndDisconnect (StopRunning + settings persist + Disconnect)
move into TRadioController, making the full connect/disconnect lifecycle callable
headless. MainForm.DoConnectDevice/DoStopAndDisconnect become thin wrappers that
only add UI-extras (status strings, AGCTop mirror, spectrum reset/resize,
offline status, A-VFO highlight). Behavior-preserving: status/highlight now run
after bring-up, consistent with the batch-29 precedent (no packets yet, safe).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OnSpectrumReady/OnWaterfallReady move into TRadioController. The controller now
owns the last spectrum/waterfall frame (FSpectrumBuf/FWaterfallBuf) for the web
mirror and emits raw pixels via OnSpectrumData/OnWaterfallData. MainForm
subscribes for FSpecView rendering (+ waterfall scroll decimation, a UI concern).
TWebAdapter.PushState now reads the frame from the controller (no buffer params).
Behavior-preserving; removes the last spectrum/waterfall data-path coupling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OnAudioReady moves from MainForm into TRadioController (radio-speaker push +
local AudioOut). The web steal becomes a route hook: controller fires
OnAudioConsume; TWebAdapter.AudioConsume mixes to mono and pushes to the web
client, returning True to skip local AudioOut. Behavior-preserving; removes the
last audio-path coupling to MainForm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CAT becomes a peer adapter over the controller, alongside WebAdapter. TCATAdapter
owns the CAT engine + serial/TCP transports and builds the TCATContext: getters
read FController directly (CAT thread), setters/commands call controller commands
marshaled via FController.Invoke. No MainForm or WebAdapter dependency — the
historical CAT->WebAdapter.OnXxx coupling (Mode/AGC/Band/etc.) is gone; CAT talks
only to the controller.
MainForm keeps CAT *settings* (FCATLastGlobal + OnCATSettingsChange + SettingsForm
wiring), now calling FCATAdapter.ApplySettings on change/connect. Removed
InitCATEngine, CATApplySettings, all CATGet*/CATSet*/CATDo*/SyncCAT* and the
FCATEngine/FCATSerial/FCATTcp/FCATSyncFreq fields.
Fixes a latent bug: CATSetFilterIdx routed a filter index through the web BW handler
as a negative-encoded value, but that branch was dead since batch 21 — CAT now calls
FController.SetFilterIdx directly. Adds StoreVfoA (store VFO A without retune when B
is active) and uses the now-real SetBand/BandUp/BandDown/TuneActiveBy commands.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With ApplyVfoA now a thin SetVfoA delegate (channel orchestration on the
OnAfterTune hook), the web Freq/FreqA handlers carry no MainForm logic — move
them to TWebAdapter.OnFreq (active-VFO routing → SetVfoA/SetVfoB) and OnFreqA
(SetVfoA). CAT freq (CATSetVfoA/SyncCATVfoA) left as-is — moves with the CAT
adapter (Phase 4).
Remaining host-coupled web handlers: Run / Connect / device CRUD (daemon entry).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that RestoreBand and XVTR enter/exit are full controller commands, the web
Band and XvtrBand handlers carry no MainForm orchestration — move them to
TWebAdapter.OnBand/OnXvtrBand (validation + SaveCurrentBand/persist + command).
CAT Ctx.DoBandByIndex and CAT band up/down repointed to FWebAdapter.OnBand.
Remaining host-coupled web handlers: Freq/FreqA (ApplyVfoA channel orchestration),
Run/Connect/device CRUD (daemon entry).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the xvtr wrapper render-extras (wideband view + web push) into the rfXvtr
render handler, and the HF-return (RestoreBand) into core DeactivateXvtr — emitting
rfXvtr *after* RestoreBand so the extras render sees the settled HF state.
MainForm.ActivateXvtrBand/DeactivateXvtr collapse to thin delegates.
ChannelController drops its OnActivateXvtr/OnDeactivateXvtr host callbacks and calls
FRadio.SetXvtrBand directly — no host coupling left, only the OnActiveChanged out-event.
Also fixes a latent regression from the OnAfterTune hook: an intermediate SetVfoA
during channel Apply (xvtr-exit → RestoreBand → retune to the old freq) fired
OnVfoTuned and deactivated the channel mid-apply. Apply is now atomic w.r.t.
channel orchestration via an FApplying guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
controller.RestoreBand now does the final receiver retune on the active VFO
(SetVfoA/SetVfoB) itself — channel orchestration rides the OnAfterTune hook — and
emits rfBandRestore so the UI resets the waterfall-average buffer (desktop-only,
fires on explicit band change, not per-tune). MainForm.RestoreBand collapses to a
thin delegate.
RestoreBand is now headless-complete: desktop band buttons, channels, connect-
restore, and (soon) web/CAT can call it directly. Preserves the active-VFO retune
(active B no longer centers on A) from the batch-15 fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Channel orchestration (deactivate-on-tune-away, auto-CTCSS/pre-channel-drive
restore) was glued to MainForm.ApplyVfoA, so only desktop/web-freq tuning ran it.
Add TRadioController.OnAfterTune, fired at the very end of SetVfoA (after the
rfVfoA render returns — outside its guard, no re-entrancy), wired to
ChannelController.OnVfoTuned.
Now every SetVfoA caller (desktop, web, CAT, encoder TuneActiveBy, SetActiveVfo,
channel apply) is uniformly channel-aware and headless-compatible. ApplyVfoA
becomes a thin delegate. Minor improvement: encoder tune-away now deactivates an
active channel like desktop; SetActiveVfo/channel-apply are no-ops (freq matches).
Foundation for moving RestoreBand's retune into the controller (Band command).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that global-settings persistence is controller-owned, fold the persist into
SetSampleRate (sample rate is global; self-gated on FDevConnected). OnSampleRateSelect
becomes a thin wrapper. Web Span moves to TWebAdapter.OnSpan (keeps the valid-span
whitelist), so the last span coupling to MainForm is gone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MakeGlobalSettings (the central persist builder, ~50 fields) lived in MainForm
and mixed radio state with UI fields, blocking headless persistence. Replace it
with TRadioController.BuildGlobalSettings (radio fields from live state) +
SaveGlobalSettings (self-gated on FDevConnected). The 7 scattered
SaveGlobal(FDevMAC, MakeGlobalSettings) call-sites collapse to FController.
SaveGlobalSettings.
Non-radio fields (theme/FPS/MHz-digits/CAT) are NOT controller state — they ride
through FLoadedGlobal (the loaded persist record the controller already holds for
round-trip). The GUI mirrors them into FLoadedGlobal at their change-points
(SetLightTheme/ApplyFPS/ApplyFreqMhzDigits/OnCATSettingsChange + connect for the
app-global theme). Controller never reads or acts on them.
Behavior-preserving (same blob, same save points); makes the daemon able to
persist radio settings without the UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-tick PushSpectrum mirror (status strings + ~40 state values + spectrum/
waterfall buffers) moves out of SpectrumTimerTick into TWebAdapter.PushState,
which reads FController for everything and takes the pixel buffers as params.
MainForm's timer now just calls FWebAdapter.PushState(FSpectrumBuf, FWaterfallBuf).
Server lifecycle/ownership stays in MainForm for now — it's still driven by the
remaining host-coupled handlers (PushXvtrToWeb, device CRUD); ownership transfer
completes when those move.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that SetDrive is a complete command, the web/CAT Drive handler moves into
TWebAdapter.OnDrive (the one handler WA1 left behind). CAT context SetDriveLevel
repointed to it too.
Makes the channel-power-clear reactive: ChannelController.OnDriveChanged fires on
the rfDrive event and clears FPreChannelPower for any external drive change
(slider/web/encoder), guarded by FRestoringDrive for the unit's own apply/restore.
This removes the imperative ClearPreChannelPower coupling so SetDrive callers need
no channel knowledge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Channels are recall-of-saved-config — a client of the radio core, not part
of DSP/network. Move ApplyChannel/CheckChannelActive + active-channel state
(FActiveChannelIdx, FPreChannelPower) out of MainForm into TChannelController,
which orchestrates controller commands and emits OnActiveChanged for the
Channel-button render. Removes the last UI-held state blocking headless mode.
Also completes TRadioController.SetDrive (was a skeleton): clamp + CalcDriveByte
+ SetDriveLevel + PushNetworkState + Changed(rfDrive), with an rfDrive render
case; routes slider/web/channel drive through it. Adds SetCurrentBandIdx for the
lightweight HF band switch.
Behavior note: channel recall now applies the mode-default filter via SetMode
(was: kept current filter — a latent quirk that left FM on an SSB bandwidth).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WebOnMic only feeds TX mic samples to the controller's WDSP engine
(no UI, thread-safe like the other engine data callbacks), so it moves
verbatim into TWebAdapter.OnMic. FWebServer.OnWebMic now points there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the pure controller-command web handlers out of MainForm into a
new TWebAdapter (bridge between TWebServer and TRadioController),
marshalling via FController.Invoke so it is daemon-ready.
- 20 handlers moved: Mode/Filter/AGC/AGCTop/Volume/WfAGC/WfNF/Mute/
NR/NB/SNB/ANF/FreqB/Center/Attn/ActiveVfo/Ctun/MOX/Tun/FMStep +
ClientActiveChanged. Each calls a controller command directly.
- CAT context (Ctx.Set*) and CATSetFilterIdx repointed to the adapter,
since CAT reuses these as its command layer.
- Host-dependent handlers (Freq/Band/XvtrBand/Span/Run/Drive/connect/
disconnect/CRUD/mic) and the outgoing PushSpectrum mirror stay in
MainForm for now (WA2/WA3); IWebHost is the placeholder seam.
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>
Factor the "start with an already-chosen device" sequence (preload sample
rate + async WDSP open + DoConnectDevice) out of BtnStartStopClick into
StartWithDevice(Dev). BtnStartStopClick's START branch now resolves the
device (dialog/autostart) and calls StartWithDevice; the web overlay will
reuse the same method to connect without the modal dialog. Behavior-preserving.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce DeviceStore.pas (TDeviceStore) as the single source of truth for
saved devices (hpsdr_devices.ini CRUD + autostart) and the current discovery
list. The controller owns one instance (created/freed in its ctor/dtor);
desktop and (later) web frontends edit/render through it so the lists never
diverge.
Refactor TDeviceDialog to use a TDeviceStore reference instead of its own
arrays + ini code. MainForm wires FDeviceDialog.Store to the controller's
store, routes discovery results (DoAddDevice) and the preload-rate lookup
through it (TDiscoveredDevice now carries the MAC), and drops the now-unused
FDevices/TDeviceItem. Behavior-preserving for the desktop dialog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fill controller.ConnectDevice with the DoConnectDevice load core: Connect,
load per-device settings by MAC (TX/Alex/XVTR, PA cal, drive%, sample rate,
NR/NB/SNB/ANF, Wf/Show/Spec params) into controller fields, configure DSP
display + open audio devices. Returns False on connect failure (UI shows the
message). New FLoadedGlobal field retains the loaded TGlobalSettings so the UI
can render fields the controller doesn't own (FPS/FreqMhzDigits/CAT).
New OnControllerState(rfDevice) renders all device-level widgets from state
(drive slider, NR/NB buttons, FSpecView/Wideband params, DUP, sample-rate
overlay, CAT apply, web push, FPS, freq digits). drive% now loads directly
into FDrivePercent instead of via TrkDrive.Position; the slider reflects it in
the render. DoConnectDevice collapses to a thin wrapper: ConnectDevice ->
RestoreBand/XVTR/active-VFO -> StartRunning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the device bring-up tail of DoConnectDevice into TRadioController,
symmetric with StopRunning:
- Relocate four pure DSP/network helpers into the controller
(BuildMicLineSelectByte, ApplyNoiseFilterButtonsToDSP,
SendDUCSpecificFromSettings, ApplyTXSettingsToDSP); external callers
(OnTXSettingsChange) now call FController.*.
- New StartRunning(SpectrumWidth): General packet, wideband/DDC config,
WDSP sample-rate sync, DSP restore (mode/volume/filter/NR/AGC), Run=1
sequence, seq reset, DDC/DUC Specific + TX chain, AGC lines. Emits
Changed(rfRunning) so the run button / MOX enable / sleep inhibit render
through OnControllerState.
DoConnectDevice's ~80-line Phase C collapses to one call; only UI bits
(FSpecView.AGCTop, status text, ResizeSpectrumPanels) stay in the wrapper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the 14 persisted display-state fields (FShow*/FSpec*/FWf*/FTXSpec*/
FWidebandFill/FSpectrumFill) from MainForm into TRadioController so a
headless device load (DoConnectDevice) can apply them without the UI. Pure
field relocation: every reader/writer now addresses FController.F*, render
still happens in MainForm against FSpecView/FWidebandView. Compiler-verified
complete; behavior preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>