Commit Graph
155 Commits
Author SHA1 Message Date
ew8bakandClaude Opus 4.8 4728e02459 feat: QO-100 beacon lock — robustness, persistence, FT8-aware UI
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>
2026-06-19 10:02:48 +03:00
ew8bakandClaude Opus 4.8 42610b304e feat: QO-100 beacon lock — stabilise LNB drift to NB transponder beacon
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>
2026-06-18 11:04:14 +03:00
ew8bakandClaude Opus 4.8 40dca01c40 fix: persist CTun center + save VHF/UHF bands (Pluto); QO-100 transverter template
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>
2026-06-17 22:02:01 +03:00
ew8bakandClaude Opus 4.8 24f74357cd feat: Pluto RX hw-gain control + AD9361 cold-init (FIR via libad9361)
- 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>
2026-06-17 12:06:59 +03:00
ew8bakandClaude Opus 4.8 a988849d3e feat: Pluto/AD936x SDR backend (discovery, RX, VHF band plan)
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>
2026-06-16 18:55:20 +03:00
ew8bakandClaude Opus 4.8 341bb249fd fix: four logic bugs in RadioController
1. StartRunning applied AGC mode via a raw enum cast (TWDSPAGCMode(FAGCMode))
   instead of the index→enum table, inverting Fast↔Off on every connect/go-live
   (GUI and daemon). Now uses ApplyAGCToEngine like the rest of the unit.
2. SetFilterIdx indexed the per-mode BW/deviation tables with an unchecked Idx
   (FM tables hold only 2 entries) — out-of-bounds when driven from CAT/web.
   Now clamps Idx per mode (EnsureRange).
3. ConnectDevice loaded FActiveVfo / FCurrentBand straight from config without
   range validation; a corrupt/old blob could leave FCurrentBand invalid and
   make later SetMode/SetFilterIdx write past FBandCache. Both now clamped.
4. VfoSwap did not run the bandstack DSP restore that SetActiveVfo does, so a
   swap to a VFO on another band kept the old mode/filter/AGC while the band
   button highlighted the new band. Now mirrors SetActiveVfo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:02:17 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 b9c358789d Phase 5 (D3): lift connect/disconnect orchestration into the controller
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>
2026-06-09 14:50:38 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 f122079b83 Phase 5 (D2): move spectrum/waterfall raw frames into the controller
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>
2026-06-09 14:46:39 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 7c8fd298ae Phase 5 (D1): move RX audio routing into the controller
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>
2026-06-09 14:43:04 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 682025e99b Phase 4: extract CAT into its own adapter (CATAdapter.pas)
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>
2026-06-09 14:29:56 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 daee5672a9 Phase 5: fill SetBand as a real band-by-index command
SetBand was a skeleton (FCurrentBand := Idx + Changed), leaving BandUp/BandDown
non-functional. Fill it with the band-change logic (XVTR-exit + persist + restore)
that WebAdapter.SyncBand had inline; SyncBand now just calls FController.SetBand.
Gives CAT band up/down/by-index a controller command to call (Phase 4) instead of
routing through the web adapter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:22:15 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 cc8380ae85 Phase 5: make XVTR enter/exit full controller commands
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>
2026-06-09 09:59:20 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 de1f4b772d Phase 5: make RestoreBand a full controller command (self-retune)
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>
2026-06-09 09:52:11 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 7b40eae174 Phase 5: add OnAfterTune hook so all tune paths run channel orchestration
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>
2026-06-09 09:49:16 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 5f5558e4a2 Phase 3 (WA2c): move web Span into the adapter; SetSampleRate self-persists
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>
2026-06-08 21:03:47 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 2c356beb77 Phase 5: move global-settings persistence into the controller
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>
2026-06-08 21:00:57 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 03f7359184 Phase 5: extract channel memory logic into ChannelController.pas
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>
2026-06-08 20:21:04 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 c4981fcb19 Phase 3 (batch 33+34): web device discovery/connect parity
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>
2026-06-08 15:30:03 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 7f7f429515 Phase 3 (batch 31a): shared TDeviceStore for saved+discovered devices
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>
2026-06-08 12:36:43 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 783c8976c9 Phase 5 (batch 30): move device connect/load-by-MAC into the controller
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>
2026-06-08 12:10:26 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 04f621bf3d Phase 5 (batch 29): move Run=1 go-live (StartRunning) into the controller
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>
2026-06-08 11:31:04 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 1fdbe27b96 Phase 5 (batch 28): move display state into the controller
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>
2026-06-08 11:15:58 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 9fdfc67f65 Phase 5 (batch 27): move Run=0 teardown (StopRunning) into the controller
Extract the graceful Run=0 teardown from BtnStartStopClick's STOP branch
into TRadioController.StopRunning: UpdateState/SetRunAndFreq(False) at zero
drive, clear FRunning/FTransmitting, SetTXRun(False), reset RX telemetry,
emit Changed(rfTransmitting) + rfRunning. The network stays Connected so the
UI wrapper can persist state before Disconnect.

New OnControllerState(rfRunning) render handles the START/STOP button,
BtnMOX.Enabled and sleep inhibit; the context-specific offline status text
stays with the caller. FormClose reuses StopRunning (same teardown).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 10:59:14 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 3a387e06ce Phase 5 (batch 26): move OnTXIQ + DUC queue into the controller
The TX IQ handler (batch WDSP TXA output into 240-sample DUC packets,
24-bit clamp, FNetwork.SendDUCIQ) moves into TRadioController.OnTXIQ,
along with the FDUCPendingI/Q/Count queue fields. The DUC-queue reset on
a TX-state change moves from the rfTransmitting render into SetMOX itself
(the render does not run headless). Both DSP-callback wiring sites
(FormCreate and RecreateDSPEngine) now point at FController.OnTXIQ.
Backend-agnostic: a Pluto backend converts 24-bit->int16 in SendDUCIQ.

Pre-existing gap noted: RecreateDSPEngine never re-wires OnPullMicSamples,
so sound-card mic is lost after a sample-rate change (unchanged here).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 23:42:22 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 c1da506f73 Phase 5 (batch 25b): move HP-status callback into the controller
OnHPStatusCB + DoUpdateStatus move into TRadioController as OnHPStatus
(network thread: board-specific supply/power/SWR decode -> pending fields
-> Invoke) and ApplyHPStatus (marshalled: PEP-style fwd/SWR ballistics +
supply EMA into FLast*, ADC-overload via Changed(rfADCOverload), and the
HW-PTT edge -> SetMOX). FNetwork.OnHPStatus is wired to the controller;
the ADC-overload indicator renders via OnControllerState(rfADCOverload).
The HWPTT->TX path is now headless-capable and reads an always-current
FWebClientActive for mic-source. TStatusUISync is left dead in UISync.pas
(unit still used for TDDCSeqSync) pending a separate cleanup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 23:35:35 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 239ff54942 Phase 5 (batch 24): move OnDDCIQ + seq telemetry into the controller
The DDC IQ receive handler (sequence-error tracking + feeding the active
DDC into WDSP) moves verbatim into TRadioController.OnDDCIQ, along with
the 12 RX/seq telemetry fields it owns (FRXPacketCount, FRXStartTime,
FRXLastPktTime, FActiveDDC, FLastDDCSeq, FLastDDCIndex, FDDCLastSeq,
FDDCSeqValid, FSeqErrorCount, FLastSeqErrorDDC, FLastSeqErrorDelta,
FSeqOkStreak). FNetwork.OnDDCIQ is wired to the controller method; all
MainForm readers (meter timer RX-running/stall detection, status-bar SEQ
text, web seq text) and the START/STOP/connect resets now go through
FController. Behaviour unchanged; backend-agnostic (a future Pluto
backend synthesises the same TDDCIQPacket).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:39:22 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 3d2d76452c Phase 5 (batch 23): move TX mic callbacks into the controller
OnMicPacket (HW mic packet -> WDSP, only when TX mic source is Radio) and
PullSoundCardMic (sound-card input -> WDSP, when source is SoundCard) are
pure data-path handlers with no UI/MainForm state, so they move verbatim
into TRadioController. FormCreate now wires FNetwork.OnMicPacket and
FDSPEngine.OnPullMicSamples to the controller's own methods. These are
backend-agnostic consumers (they operate on the common interchange
format), so they belong in the core regardless of HPSDR vs future Pluto
backends. Behaviour unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:33:11 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 8be98b72e1 Phase 5 (batch 22): move engine lifecycle into the controller
TRadioController now owns its engines: CreateEngines(ASampleRate) builds
FNetwork/FDSPEngine/FAudioOut/FAudioIn (and the constructor now owns
FSettings), FreeEngines closes and frees them, and Destroy calls
FreeEngines + frees FSettings. FormCreate replaces the four inline
T...Create calls with a single FController.CreateEngines (after the
startup sample rate is loaded, so FDSPEngine is still born at the right
rate) and keeps wiring the MainForm callbacks. FormDestroy keeps the
graceful Run=0 stop + Disconnect but drops the per-engine Close/Free and
FSettings.Free, which now happen in the controller's destructor. This is
a behaviour-preserving ownership move toward headless/daemon operation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:20:17 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 33c433ce5e Phase 3 (batch 21): route web Mode/Filter through controller commands
SyncWebMode now delegates to FController.SetMode (default filter + DSP +
band cache; render via rfMode), and SyncWebFilter to FController.SetFilterBW,
whose stub is filled in: it applies the requested bandwidth and highlights
the nearest preset (render via rfFilter). The JS client only ever sends a
bandwidth in Hz ({cmd:"filter",bw:N}), so the old negative-index branch
was dead code and is removed. Both web handlers now use the same controller
commands as the desktop; the duplicated inline mode/filter logic is gone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:06:18 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 492b29bea1 Phase 3 (batch 19): move TUN (tune tone) into the controller
SetTune now carries the TUN engine logic: the DoNotTx/RXOnly safety
gates, enabling the WDSP PostGen tone before TX, the TUN-level drive
(CalcDriveByte sees FTuning), driving TX on/off via SetMOX, and the
network re-push on exit. MainForm.ApplyTUN becomes a thin wrapper
(mirrors FWebClientActive, calls SetTune); the BtnTUN style render moves
to OnControllerState(rfTuning). BtnMOXClick still exits tune via
ApplyTUN(False).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 16:40:01 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 603e0850cf Phase 3 (batch 18): move MOX (TX core) into the controller
SetMOX now carries the TX engine/network logic: DoNotTx/RXOnly safety
gates, drive recalc, the PTT relay sequence (relay-before-PTT on TX-on,
PTT-before-relay on TX-off), mic-source selection (web/HW-PTT/default),
KeepRXDuringTX, SetTXRun, and the non-DUP TX->RX FlushRX + post-TX mute.
New controller fields FWebClientActive (mirrored from the web server in
the thin ApplyMOX wrapper) and FWebMicActive (moved from MainForm), plus
DefaultMicSource. UI render (BtnMOX style, FSpecView TX overlay/mode,
grid, FDUCPendingCount reset) moved to OnControllerState(rfTransmitting).

Also fix a pre-existing status-bar bug: web TX was labelled 'TX radio
mic' (the old if/else folded any non-soundcard source into radio); now
a case shows 'TX web mic' for txmsWeb.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 16:35:54 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 c249b46b17 Phase 3 (batch 17): move CTUN center scroll (SetCenter) into the controller
SetCenter was a Phase-1 skeleton; filled it with the CTUN view-scroll
logic: set FCenterFreq, shift the demodulator by (active VFO - center),
push a full HP frame, and emit Changed(rfCenterFreq). The UI renders via
OnControllerState (invalidate ruler cache + deferred spectrum redraw, since
a drag emits many events).

Both entry points now call FController.SetCenter: the desktop CTUN drag
(DoSpectrumDrag) and the web center scroll (SyncWebCenter). Removes the
duplicated inline shift/network logic from both.

Side effect / latent fix: the web center scroll previously only called
UpdateState without SendFullHP (the desktop path did both), so a web scroll
did not push the new DDC center to the radio immediately. Routing it through
PushNetworkState now sends the full HP frame like the desktop path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 14:00:46 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 831e5c3a31 Phase 3 (batch 16): move XVTR activate/deactivate into the controller
ActivateXvtrBand/DeactivateXvtr followed the Band pattern: the state +
DSP + network now live on TRadioController; the UI renders via events.

- ApplyXvtrToNetwork moved to the controller (XVTR enable bit / DisablePA
  / RX antenna). MainForm.ApplyXvtrToNetwork is a thin delegate.
- ActivateXvtr builds a TBandSettings from the XVTR slot and runs it
  through the shared ApplyBandDSP (mode/filter/AGC/CTUN/FM), then sets
  VFO B (range-clamped) / VFO A / center / drive and pushes to the radio.
  Render is driven by Changed(rfXvtr) + Changed(rfActiveVfo). The slot has
  no stored filter bandwidth, so FilterBWFor(mode, idx) derives it from
  the per-mode table without changing the index.
- DeactivateXvtr saves the slot via SaveCurrentBand (its XVTR branch
  already writes the whole slot) and exits; the HF return (RestoreBand +
  final active-VFO retune) stays in the UI wrapper.
- SetXvtrBand is now a real dispatcher (>=0 activate, <0 deactivate).
- rfBand render is XVTR-aware (clears HF band buttons while in XVTR);
  added an rfXvtr render case for the XVTR button highlight.
- The thin UI wrappers keep only wideband-view + web push. ~150 lines of
  inline logic removed from MainForm; all callers (web/channel/band-click/
  startup) are unchanged and go through the wrappers.

Also fixes a latent filter-restore bug surfaced by the XVTR round-trip
(transverter -> HF -> transverter): the rfMode render calls
UpdateFilterButtons, which resets the non-FM filter to the mode default.
That reset belongs to a mode change (the controller already applies it via
ApplyModeDefaults), not to a passive render, so it clobbered the filter
restored from the band/XVTR cache. rfMode now preserves FFilter/FFilterBW
around UpdateFilterButtons. This also fixes the same loss on plain HF band
switches for non-FM modes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 13:49:31 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 e09bc21bba Fix active-VFO desync; add bandstack-on-VFO-switch and per-XVTR VFO-B
Active-VFO desync (batch 15 follow-up): DoConnectDevice loaded
FActiveVfo from settings but never re-rendered the highlight (it was
drawn once in FormCreate while the field was still 0) nor re-tuned the
receiver. Result on reconnect with B last-active: the engine (spectrum,
filter band, mouse tuning) ran on VFO B while the UI showed A active.
Now the connect path brings both the receiver and the highlight to the
actually-active VFO via SetActiveVfo, and RestoreBand's final retune
follows the active VFO (A -> ApplyVfoA, B -> SetVfoB) instead of always A.

Bandstack on VFO switch: switching the active VFO to one parked on a
different ham band now restores that band's DSP state (mode/filter/AGC/
CTUN/FM). Extracted ApplyBandDSP (the DSP half of RestoreBand, without
the VFO frequencies) and reused it from both RestoreBand and SetActiveVfo.
VFO frequencies stay independent (each VFO carries its own band);
continuous tuning across a band edge still does not recall (band button
only) as on real radios. SaveCurrentBand no longer writes a VFO frequency
into a band's memory unless that VFO is actually within the band, so a
40m VFO-A no longer pollutes 30m's stored VfoA.

Per-transverter VFO-B: TXvtrEntry gains LastFreqB (load/save/default +
range clamp). ActivateXvtrBand restores VFO B, Deactivate/SaveAllAndExit
persist it, and SetVfoB clamps to the transverter range like SetVfoA.
The connect-time guard that forced VFO A in XVTR is dropped since B is now
a valid, in-range frequency there too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:45:11 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 24102343ea Phase 3 (batch 15): move band save/restore into the controller
Per the architecture decision, band state lifecycle belongs on the
controller (it operates entirely on controller state + engines;
persistence is already delegated to Settings.pas). A separate unit would
need friend access to ~20 fields + engines — worse coupling.

- MakeBandSettings, SaveCurrentBand: moved verbatim (pure over controller
  state; SaveCurrentBand handles the XVTR-slot vs HF band-cache split and
  delegates the write to FSettings). MainForm.SaveCurrentBand is now a thin
  delegate (22 call sites unchanged); MakeBandSettings removed from MainForm.
- RestoreBand: state + DSP application moved to the controller; rendering is
  driven by per-field Changed events (rfBand/rfMode/rfFilter/rfAGC*/rfFM*/
  rfCTun/rfVfoB). The FM filter-index sanitize (which UpdateFilterButtons
  did inline as a render side-effect) is extracted into a pure
  SanitizeFilterForMode so the DSP ApplyModeFilter still runs on the
  sanitized BW (preserves original ordering). Fixes the same AGC index-cast
  bug as earlier batches (now ApplyAGCToEngine).
- MainForm.RestoreBand is a thin wrapper: ResetWfAvgBuf (band-switch-only,
  not per-tune) + controller.RestoreBand + ApplyVfoA(FVfoA) for the final
  tune with channel orchestration (auto-CTCSS / pre-channel drive).
- Added rfFMStep render case (BtnFMStep on/off was styled ad-hoc).

Builds clean, links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:37:02 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 a8a192a3a3 Phase 3 (batch 14): route VFO swap / A>B / B>A through controller
Filled the three VFO-transfer commands (the skeletons were also backwards:
VfoACopyB stored A:=B etc).

- VfoSwap: swaps A/B, retunes the receiver to the active VFO freq
  (center=freq, shift 0), HF band-detect, network push. Fires
  rfVfoA/rfVfoB/rfBand. UI handler is thin (+ ruler-cache invalidate, since
  a swap is a large freq jump).
- VfoACopyB (A>B): delegates to SetVfoB(FVfoA) — store B:=A, retune when B
  active, render via rfVfoB.
- VfoBCopyA (B>A): copies B->A; when A active goes through SetVfoA, else
  just fires rfVfoA. The desktop B>A handler still calls ApplyVfoA for the
  active-A case to preserve channel orchestration (auto-CTCSS / pre-channel
  drive), matching the original (A>B never had that path).

Net simplification; builds clean, links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:17:50 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 9f7a48f89a Phase 3 (batch 13): route active-VFO switch through controller command
- TRadioController.SetActiveVfo(Idx): switches the active RX VFO and retunes
  the receiver to its frequency. CTUN handling preserved: if the new VFO's
  freq is visible in the current window, only the demodulator shift changes
  (DDC/network untouched); otherwise CTUN is dropped and the window centers
  on the freq. HF band-detect + network push as before. Fires
  Changed(rfActiveVfo) (+ rfBand / rfCTun when those change).
- OnControllerState(rfActiveVfo): UpdateVfoDisplay (freq displays + RX/TX
  button highlights) and a full spectrum/waterfall/ruler redraw.

MainForm.ActivateVfo is now a thin delegate; all callers (VFO select/TX
buttons, freq-display clicks, web ActivateVfo) go through it unchanged.
Band-detect keeps the historical no-drive-recalc behavior. Builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 20:47:35 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 af85ddece6 Fix TX following active RX VFO instead of the TX selector
The TX VFO is chosen independently of the RX VFO via the TX-A/TX-B buttons
(FSplitTxB: False=A, True=B); UI highlights TX-A when not split. But the TX
frequency and TX overlay used FActiveVfo in the non-split branch, coupling
TX to the active RX VFO. Result: with RX on B and TX-A selected, the radio
still transmitted on B (TX overlay also drew on B).

Make the TX VFO depend solely on FSplitTxB in all three spots:
ActiveTXFreqHz and both TXVfoIndex assignments (live update + ApplyMOX).
Now RX=B / TX=A works as the UI indicates. Pre-existing bug, not a refactor
regression. Builds clean, links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 20:40:40 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 8f0ba64d67 Fix CTUN not working on VFO B
Two A-hardcoded spots broke CTUN whenever VFO B was the active receiver:
- SetVfoB forced shift=0 (no CTUN handling), so click/wheel/edit on B
  re-centered the DDC instead of shifting the demodulator.
- SetCTun, on disable, re-centered on FVfoA regardless of active VFO.

Extract the CTUN shift/center/edge-scroll logic into a shared private
ApplyTuneCore(VfoHz) used by both SetVfoA and SetVfoB, and add ActiveVfoHz.
SetCTun now centers on ActiveVfoHz. CTUN behaves identically on A and B.

This also was a latent gap pre-refactor (the old inline VFO B tune blocks
never honored CTUN). Builds clean, links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 20:35:25 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 eb10283c85 Phase 3 (batch 12): consolidate VFO B tuning into controller command
The "set VFO B" engine block was duplicated inline across three tune paths
(DoSpectrumClick, DoSpectrumDrag, FreqDispBChanged), each subtly different
(drag had no band-detect/ruler; click had no drive recalc).

- TRadioController.SetVfoB(Hz): always stores FVfoB; when B is the active
  VFO, retunes the receiver (DDC=VFO B, no CTUN, shift 0), HF band-detects
  (FCurrentBand + drive recalc -> Changed(rfBand)) and pushes to the radio;
  when B is inactive (split, listening on A) it only stores the freq for TX.
  Fires Changed(rfVfoB).
- OnControllerState(rfVfoB): renders FreqDispB and, when B active, the
  spectrum/ruler (deferred via FSpectrumDirty, matching the rfVfoA path).

The three call sites collapse to a single FController.SetVfoB(...) each;
FreqDispBChanged is now a one-liner. Behavior unified: click/FreqDispB drop
from sync DrawSpectrum to deferred (consistent with VFO A click-to-tune,
imperceptible); drag gains band-detect + ruler refresh; click gains drive
recalc on band change. Net -23 lines.

Left inline (not identical): DoWidebandClick VFO B (extra XVTR clamp +
wideband-specific render) and SyncWebFreq (uses SetRunAndFreq, the web/run
network path, not UpdateState+SendFullHP). Builds clean, links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 20:25:55 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 908c02d08a Phase 3 (batch 11): route VFO A tuning core through controller (keystone)
ApplyVfoA is the keystone of the heavy class — click-to-tune, drag, wheel,
web freq and RestoreBand all funnel through it, and it was the most
UI-coupled method. Split per the long-standing plan: pure frequency engine
into the controller, channel orchestration + render left in UI.

- TRadioController.CalcDriveByte: moved verbatim (it was already pure over
  controller state — reads only band/xvtr cal, TUN/drive%, PA max, no
  widgets). MainForm.CalcDriveByte is now a thin delegate (17 call sites
  unchanged). Needed by SetVfoA's band-detect.
- TRadioController.SetVfoA(Hz): XVTR clamp, FVfoA, CTUN shift/center with
  filter-edge center-scroll (exposes FLastVfoScrolled for the waterfall),
  HF band-detect (FCurrentBand + drive recalc -> Changed(rfBand)), FM
  auto-repeater (UpdateRptAutoState), PushNetworkState, Changed(rfVfoA).
  TuneActiveBy (the Arduino encoder path) now fully tunes the radio.
- PushNetworkState made public (command + UI pre-channel-drive both use it).

MainForm: ApplyVfoA is now thin — calls SetVfoA, then the channel-only
orchestration (auto-CTCSS reset, pre-channel drive restore via TrkDrive
widget + re-push), then CheckChannelActive. The auto-RPT reset block was
dropped: SetVfoA's UpdateRptAutoState already yields the same net state and
running it after SetVfoA would have wrongly cleared a freshly auto-set
MINUS (ordering hazard the memory flagged). Render (FreqDispA, spectrum,
ruler, band buttons) moved to OnControllerState(rfVfoA/rfBand), so every
caller — encoder/web/CAT included — updates the display for free.

Builds clean, links. Click-to-tune/drag/wheel/band still go through this
path; VfoB / ActiveVfo / Band(RestoreBand) / Xvtr still pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 20:17:45 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 6edcf8bdf4 Phase 3 (batch 10): route sample-rate/span change through controller
Span ≡ sample rate in this app; OnSampleRateSelect (overlay dropdown) is a
fairly self-contained heavy-class item.

- TRadioController.SetSampleRate(Hz): sets FSampleRate/FSpanHz + band cache,
  recreates the WDSP channel (ChangeSampleRate = Close+FlushQueue+Open),
  pushes the new DDC rate to the radio, and restores full DSP state
  (mode/volume/filter/NR/NB/SNB/ANF/AGC-top/AGC/shift).
- Fixes the same latent AGC cast bug as earlier batches: the inline restore
  used SetAGC(TWDSPAGCMode(FAGCMode)) (index cast); now goes through the
  shared ApplyAGCToEngine mapping.

MainForm: OnSampleRateSelect is a thin wrapper (global-settings SaveGlobal
stays in UI). OnControllerState(rfSampleRate) renders the rate overlay and
recomputes AGC display lines (UpdateAGCLines needs UI spectrum width).
Dropped the cosmetic ProcessMessages pre-paint. Startup paths that set
FSampleRate directly + RecreateDSPEngine left as-is. Builds clean, links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 20:03:59 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 967e7cb7b1 Phase 3 (batch 9): route FM repeater shift through controller commands
FM Rpt is the first FM-Rpt heavy-class item (XvtrTranslate/ActiveTXFreqHz
blocker already lifted in d895800). Following the established pattern:

- TRadioController.SetFMRpt(Dir): explicit direction, clears auto-active,
  pushes network state; rendered via OnControllerState(rfFMRpt).
- SetFMRptOffset(Hz): updates offset, re-pushes when RPT engaged.
- UpdateRptAutoState: 2 m auto-MINUS logic moved into controller, fires
  Changed(rfFMRpt) instead of inline StyleButton.
- Extracted private PushNetworkState helper (full HP frame); SetCTun now
  reuses it (dedupes the UpdateState+SendFullHP block).

MainForm: Btn/EdFMRpt handlers are thin wrappers (toggle decision +
SaveCurrentBand stay in UI); UpdateRptAutoState delegates to controller;
ApplyFMRpt removed. OnControllerState(rfFMRpt) restyles both buttons and
sets the offset edit text only when unfocused (avoids disrupting typing).
Direct FFMRptDir writes in band/channel/xvtr restore paths left as-is
(migrate with their own commands later). Builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:56:56 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 f12d2a69ad Phase 3 (batch 8): route desktop Mode/Filter through controller commands
- FILT_* таблицы (BW/DEF/DEV/COUNT) перенесены MainForm -> Settings.pas
  (нужны контроллеру для headless-выбора фильтра); UI-подписи FILT_*_NAMES
  остались в MainForm (ссылки разрешаются через uses Settings).
- ApplyModeFilter (чистый DSP) перенесён в TRadioController; все вызовы в
  MainForm -> FController.ApplyModeFilter.
- Контроллер: ApplyModeDefaults (дефолтный фильтр/девиация под FMode,
  зеркало state-части UpdateFilterButtons) + наполнены команды SetMode
  (FMode + дефолт + DSP SetMode/TUN + ApplyModeFilter + band cache) и
  SetFilterIdx (FFilterBW по таблицам режима + ApplyModeFilter + band cache).
- BtnModeClick/BtnFilterClick — тонкие обёртки к командам; рендер
  (StyleButton + UpdateFilterButtons + SyncSpecViewFreq) в
  OnControllerState(rfMode/rfFilter).

OnModeFilterSelect (VFO overlay), SyncWebMode/SyncWebFilter (web/CAT),
RestoreBand пока зовут FController.ApplyModeFilter инлайн — мигрируют
в следующих батчах (как и в прошлых: web/overlay позже).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 16:34:35 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 2e4cd3cc65 Phase 3 (batch 7): route CTun toggle through controller command
Первая команда тяжёлого класса. TRadioController.SetCTun несёт логику
движка/сети (band cache, при выключении — центрирование на VFO + SetShift(0)
+ UpdateState/SendFullHP); все нужные поля и хелперы XvtrTranslate/
ActiveTXFreqHz уже в контроллере. BtnCTunClick — тонкая обёртка
(SetCTun(not FCTun)); рендер (StyleButton/SyncSpecViewFreq/перерисовка
спектра+водопада) ушёл в OnControllerState(rfCTun).

Прямые записи FController.FCTun в путях band restore / ActivateVfo
оставлены — мигрируют со своими командами (Band/VFO).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:49:18 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 d895800f62 Phase 3 (heavy prep): move XvtrTranslate/ActiveTXFreqHz into controller
Перенос частотных хелперов (общий блокер для Vfo/Mode/Band/CTun/TX/FM-Rpt)
в TRadioController как чистых read-only функций над состоянием. Все
константы (CFG_XVTR_COUNT, MODE_FM, RPT_MINUS/PLUS) уже доступны через
Settings/WDSPEngine/FMRepeater в uses контроллера. MainForm.XvtrTranslate
и MainForm.ActiveTXFreqHz стали тонкими обёртками-делегатами — ~40 мест
вызова в UI/сетевом слое работают без изменений.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:28:33 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 fd145b28f6 Phase 3 (batch 6): route FM squelch + CTCSS through controller commands
The user plans to expose FM over web/CAT later, so FM controls get the
same controller treatment now — future web/CAT FM commands plug straight
into these.

- Relocate CTCSS_COUNT/CTCSS_TONES from MainForm to FMRepeater (a shared
  FM unit MainForm already uses) so TRadioController can drive CTCSS on the
  DSP. CTCSS_NAMES stays in MainForm (UI dropdown labels). MODE_FM is
  already visible to the controller via WDSPEngine.
- SetFMSquelch/SetFMSquelchLevel apply FM squelch (mode-gated) to the DSP;
  SetFMCTCSS/SetFMCTCSSTone clamp and apply CTCSS. All fire OnStateChanged.
- OnControllerState restyles the SQ/CTCSS buttons, syncs the squelch slider
  + label, and the CTCSS tone dropdown. Handlers/SetFMCTCSSTone call the
  commands (manual CTCSS still clears the auto-active flag in the handler).

FM repeater stays for the heavy batch — it needs XvtrTranslate/
ActiveTXFreqHz moved into the controller. ApplyFMSquelch remains for
band/mode re-apply paths (RestoreBand, XVTR).

Builds clean (cocoa).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:15:46 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 7d3570346f Phase 3 (batch 5): route FM step through controller command
FM step is the only FM control exposed to another frontend (web:
SyncWebFMStep in, FWebServer.FMStepIdx out), so it's the FM param with
real cross-frontend value.

- TRadioController.SetFMStep clamps and fires OnStateChanged(rfFMStepIdx).
- OnControllerState updates the step dropdown, pushes FMStepIdx to the web
  server, and sets the FSpecView FM grid step (when in FM mode).
- MainForm.SetFMStep is now a thin wrapper over the command, so existing
  callers (band restore, dropdown select, web sync, XVTR restore) are
  unchanged.

FM squelch/CTCSS/repeater are intentionally left as-is: they are UI-only
(no web/CAT/daemon exposure) and already operate on controller state
fields, so routing them through commands adds ceremony without value for
the swappable-UI/daemon goals; CTCSS would also need CTCSS_TONES relocated
to a shared unit and repeater needs the network helpers (XvtrTranslate/
ActiveTXFreqHz) that belong to the heavy-command batch.

Builds clean (cocoa).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:10:44 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 ad21516d48 Phase 3 (batch 4): route RX step attenuator through controller
SetAtten clamps, applies to the network (SetStepAtten), and fires
OnStateChanged; SyncWebAttn calls it. No desktop widget exists for the
attenuator, so there's no OnControllerState case — but state and the web
snapshot now flow through the single command path like everything else.

Builds clean (cocoa).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:00:04 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 b3e4360a5d Phase 3 (batch 3): route Volume, AGC-top, WfAGC/WfNF through controller
First sliders through the controller, exercising the FSyncingFromController
re-entrancy guard:
- SetVolume/SetAGCTop clamp, apply to the DSP engine, update band cache,
  and fire OnStateChanged. SetAGCTop reuses ApplyAGCToEngine (the unified
  AGC index->enum mapping), fixing the same bad TWDSPAGCMode(index) cast
  that TrkAGCChange/SyncWebAGCTop used when re-applying AGC on level change.
- OnControllerState updates the slider/label/spectrum (rfAGCTop also
  refreshes the FSpecView AGC line, now consistent for web-initiated
  changes too) and the FSpecView waterfall flags (rfWfAGC/rfWfNF).
- TrkVolumeChange/TrkAGCChange (guarded), SyncWebVolume/AGCTop/WfAGC/WfNF,
  and ApplyWfAGCNF all call the commands.

Builds clean (cocoa).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 14:58:03 +03:00