Commit Graph
70 Commits
Author SHA1 Message Date
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 0a0a5c455e Phase 3 (WA2e): move web Freq/FreqA handlers into the adapter
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>
2026-06-09 10:18:59 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 7630172e50 Phase 3 (WA2d): move web Band/XvtrBand handlers into the adapter
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>
2026-06-09 10:09:06 +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 e1ecf947b8 Phase 3 (WA3): move outgoing web state-mirror into the web adapter
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>
2026-06-08 20:36:01 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 e5a13096dc Phase 3 (WA2b): move web Drive handler into the web adapter
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>
2026-06-08 20:26:30 +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 06fd730ff4 Phase 3 (WA2a): move WebOnMic into the web adapter
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>
2026-06-08 16:15:52 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 2f8b0bdf48 Phase 3 (WA1): extract web adapter into WebAdapter.pas
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>
2026-06-08 16:03:49 +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 9dec47a6ca Phase 3 (batch 32): extract StartWithDevice from BtnStartStopClick
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>
2026-06-08 12:49:09 +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 452795dc59 Fix: rewire OnPullMicSamples in RecreateDSPEngine
RecreateDSPEngine rebound OnAudio/OnSpectrum/OnWaterfall/OnTXIQ onto the
freshly created DSP engine but not OnPullMicSamples, so the sound-card mic
path was lost after a sample-rate change. Rebind it alongside the others.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 10:50:24 +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 9d2324cc6c Phase 5 (batch 25a): de-dup telemetry fields + push web-client-active to core
Prep for moving the HP-status callback into the controller:
- Remove the duplicate FLast{SMeter,FwdW,SWR,SupplyV,SupplyA,PLLLock}
  fields from MainForm; the controller (which already declared them) is
  now the sole owner. The UI-only S-meter ballistics (FSMeterPeak/Min/Avg)
  stay in MainForm.
- TWebServer gains an OnClientActiveChanged event (fired via a
  SetClientActive setter at the three client connect/disconnect points);
  MainForm mirrors it into FController.FWebClientActive. The lazy mirror
  in ApplyMOX/ApplyTUN is removed, so the flag is always current — needed
  for the HWPTT->SetMOX path that moves into the controller next.

Behaviour-preserving (the flag held the same value at MOX time before).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:55:13 +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 c7d93c2e46 Phase 3 (batch 20): unify web/CAT VFO-B onto the SetVfoB command
SyncWebFreq (VFO-B branch), SyncWebFreqB and SyncCATVfoB each carried a
copy of the pre-SetVfoB inline VFO-B block that pushed the radio via
SetRunAndFreq(True,...). That reset the HP sequence counter on every
freq change (only correct at Run start), left FIsTransmitting/Alex flags
stale, skipped the drive recalc on a band crossing, and forced shift=0
(ignoring CTUN). All three now delegate to FController.SetVfoB, so web,
CAT and desktop share one path (ApplyTuneCore + PushNetworkState) with
rendering via OnControllerState(rfVfoB/rfBand). ~70 lines removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 16:55:04 +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 38947113f5 Fix wheel over a VFO display tuning both that VFO and the active one
TFreqDisplay.DoMouseWheel already tunes its own VFO per-digit when the
wheel is used over it. But the form-level FormMouseWheel also fired for the
same event (Cocoa routes the wheel to the form too) and tuned the active
VFO, so scrolling over the VFO B display changed both B (via the widget)
and A (via the form handler).

Skip FormMouseWheel when the cursor is over either FreqDisplay; let the
widget handle per-digit tuning there. Uses Mouse.CursorPos for the hit-test
(the event's MousePos is not in screen coordinates on Cocoa). The form
handler still tunes the active VFO over the spectrum/waterfall/empty areas.
Pre-existing bug.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:07:12 +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 9d48f26dd8 Fix mouse wheel always tuning VFO A regardless of active VFO
FormMouseWheel quantized from FVfoA and always called ApplyVfoA, so the
wheel tuned VFO A even when VFO B was active. Pre-existing bug (the wheel
handler never branched on FActiveVfo), surfaced while testing VFO B.

Quantize from ActiveVfoFreq and route to SetVfoB when B is active.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 20:31:04 +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 05bb68ac58 Fix ruler lag on click-to-tune and VFO change
ApplyVfoA (общий путь click-to-tune/wheel/VFO/band) синхронно дёргал
PbSpectrum.Invalidate, но не PbRuler.Invalidate — линейку обновлял лишь
таймер через NeedsRulerRedraw-гейт, отсюда "не всегда" при CTUN off.
Invalidate только планирует перерисовку (не тяжёлый рендер), а DrawRuler
дешёвый, поэтому дёргаем PbRuler.Invalidate сразу рядом с PbSpectrum.

Явная инвалидация Ruler из RestoreBand убрана как избыточная — band
идёт через ApplyVfoA, теперь единый источник правды там.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 16:15:03 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 af1352b1e6 Fix ruler not redrawing on CTun toggle and band switch
Ruler (PbRuler) — отдельный paintbox, перерисовывается только по
PbRuler.Invalidate. Дискретные события его не дёргали:
- CTun toggle (batch 7): OnControllerState(rfCTun) рисовал спектр/водопад,
  но не линейку. Добавлен InvalidateRulerCache + PbRuler.Invalidate.
- Смена диапазона: RestoreBand -> ApplyVfoA, а ApplyVfoA намеренно
  откладывает рендер на таймер (FSpectrumDirty) ради wheel/drag — что
  для оффлайна/без RUN не перерисовывало линейку. Добавлена синхронная
  инвалидация Ruler в конце RestoreBand (дискретное событие, флуда нет).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 16:09:21 +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
Uladzimir KarpenkaandClaude Opus 4.8 f76533f209 Phase 3 (batch 2): route AGC mode through controller command
TRadioController.SetAGCMode now owns the index->WDSP mapping, applies to
the DSP engine, and updates the band cache, then fires OnStateChanged.
OnControllerState restyles the AGC button group. BtnAGCModeClick,
OnVfoOverlayAGCChange, and SyncWebAGC all call the command.

Fixes a latent web bug: SyncWebAGC used TWDSPAGCMode(index) directly,
mapping the UI index (0=FAST..4=OFF) onto the wrong enum (0=OFF...), so
web AGC selections set the wrong DSP mode. The button/overlay path used
the correct AGCModes[] array; unifying on the command applies that to web
too. Buttons/overlay behavior unchanged.

Builds clean (cocoa).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:33:45 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 ff42889fa1 Phase 3 (batch 1): route NR/NB/SNB/ANF through controller commands
Apply the slice pattern to the noise-filter controls:
- TRadioController.SetNR/SetNB/SetSNB/SetANF now clamp and apply to the DSP
  engine, then fire OnStateChanged.
- OnControllerState refreshes the matching button (UpdateNR/NB/SNB/ANF).
- Click handlers, SyncWebNR/NB/SNB/ANF, and OnVfoOverlayDSPChange all call
  the commands instead of poking fields/widgets and the DSP directly.

UI clicks, the web client, and the VFO overlay now share one path; the
web client already renders these fields in applyState, so UI->web syncs.
Builds clean (cocoa); behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:20:40 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 a85f196c97 Phase 3 (slice): make Mute sync UI->web in the web client
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>
2026-06-04 11:16:26 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 b577a0d67a Phase 3 (slice): event-driven UI plumbing + Mute through controller
Vertical slice to establish the Phase 3 pattern before replicating it
across all commands: logic moves into controller commands, the UI becomes
event-driven, and frontends call commands instead of poking widgets.

- TRadioController.SetMute now carries the real logic (state + FDSPEngine.
  SetMute), then fires OnStateChanged(rfMute).
- MainForm subscribes via OnControllerState (controller -> UI). It restyles
  BtnMute from controller state. FSyncingFromController guards against
  re-entrancy when a programmatic widget change would retrigger OnChange.
- BtnMuteClick -> FController.ToggleMute; SyncWebMute -> FController.SetMute.
  The button/web no longer own the mute logic or the restyle.

Proves the command/event/refresh loop end-to-end (UI click and web both
route through the controller). Remaining commands follow this pattern.
Builds clean (cocoa); behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:01:39 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 2cf01036a3 Phase 2: move widget-stored state into TRadioController
NR/NB modes, SNB/ANF flags, and drive % were stored only in the widgets
themselves (Btn*.Tag, TrkDrive.Position) — the last hard blocker to a
headless/daemon mode, since without those controls the state had nowhere
to live and CAT/Web read them directly.

- NR/NB: BtnNR.Tag/BtnNB.Tag -> FController.FNRMode/FNBMode (Integer).
- SNB/ANF: BtnSNB.Tag/BtnANF.Tag (0/1) -> FController.FSNB/FANF (Boolean).
- Drive %: TrkDrive.Position -> new FController.FDrivePercent; the slider
  is now pure input (TrkDriveChange pushes its value into the controller),
  while FDriveLevel stays the derived hardware byte.

Buttons/sliders become pure view+input: click handlers and UpdateXButton
read/write the controller field; CAT getters, the web snapshot, and
GetSnapshot all source state from the controller, not the widgets.

No behavioral change. Builds clean (cocoa).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:48:51 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 63eb2c701f Phase 1: extract TRadioController (state + engine ownership)
Decouple MainForm from CAT/Web by introducing a headless core,
TRadioController, as the single home for radio state and engine objects.
This is groundwork for swappable UIs, a daemon (Web-only) mode, and a
future hardware control panel — each becomes a frontend/adapter over the
same controller API.

RadioController.pas (new):
- Radio state + engine objects (Network/DSP/Audio/Settings/Channels) as
  public fields named exactly as in MainForm, so wiring is mechanical.
- Command API: absolute Set* plus relative *By/Up/Down (for encoders).
- OnStateChanged(field) notifications and GetSnapshot for frontend sync.
- Invoke/OnInvoke (TUIInvokeEvent) thread-marshalling seam (GUI uses
  TThread.Synchronize; daemon supplies its own).
- Commands are scaffolding for now (update field + fire event); engine
  logic stays in MainForm and migrates in later phases (TODO-wiring).

MainForm.pas:
- Owns FController; creates it first in FormCreate, frees it last in
  FormDestroy. Engines are still created/freed by MainForm into the
  controller's fields (ownership move is a later phase).
- ~1400 FXxx references redirected to FController.FXxx (implementation
  only); dead field declarations removed from the interface.

No behavioral change — pure state relocation. Builds clean (cocoa).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:43:01 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 aff17aa3a6 Add Audio tab toggle for transceiver built-in speaker
Adds a "Send audio stream to radio (built-in speaker)" checkbox on the
Settings → Audio tab. It is a single switch for the radio's built-in
speaker:

- ON  — RX audio is streamed to the radio via the DDC Audio packet
        (port 1028, 64 L/R pairs per packet) and the speaker is unmuted
        (High Priority byte 1400 bit1 = 0).
- OFF — speaker is muted (byte 1400 bit1 = 1, ANAN-8000DLE IO1 output)
        and no audio stream is sent.

The float->int16 conversion and 64-pair packet accumulation live in
THPSDRNetwork (SendSpeakerAudio/SetSpeakerAudio), next to SendDDCAudio;
MainForm only reads the setting and forwards the audio. State is
persisted per-MAC (send_audio_to_radio, default off) and re-applied on
each radio start.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 08:55:36 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 655d242d02 Add Spectrum gradient fill toggle in settings
Mirror the existing Wideband gradient fill option for the main spectrum:
a "Spectrum gradient fill" checkbox on the Display settings tab gates the
under-curve gradient in TSpectrumView.DrawSpectrum. The curve, grid,
filter band and VFO markers always draw; only the gradient is optional.

Persisted as spectrum_fill in settings JSON (default True, preserving
current appearance). Wired through Settings -> SettingsForm visibility
event -> MainForm -> TSpectrumView.FillSpectrum, same path as
WidebandFill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 15:24:33 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 2a7bf8528a Cache SampleRateOverlay to a bitmap instead of redrawing per frame
The span-selector panel was rendered directly onto the spectrum canvas
every frame via DrawSelf, doing ~30 TextWidth + ~11 TextOut Canvas calls.
On Cocoa these text ops are very expensive (~10.6 ms/frame, ~30% of a
core) and dominated DrawSpectrum.

Render the panel once into a per-pixel-alpha cache bitmap and only
composite it over the spectrum each frame; rebuild only when state
changes (rate, hover, hidden, bounds). The cache is built with the
two-background method (render over black and white, derive straight
alpha + color) so layered semi-transparent button fills and antialiased
text composite over the live spectrum exactly as before.

Cuts the overlay phase from ~10.6 ms to ~0.2 ms per frame; DrawSpectrum
drops ~13.8 ms -> ~2.7 ms. Mirrors the caching VfoOverlay already uses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 15:10:30 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 c3a8a14110 Drop per-frame alpha-restore pass on macOS spectrum
On Cocoa the full-bitmap alpha-restore pass is unnecessary: Canvas
operations keep alpha at $FF and the cached grid background is opaque,
so removing it cuts a full-frame O(W*H) pass per spectrum frame with no
visual change (verified on macOS).

Kept under {$IFNDEF DARWIN} for Qt6/win32 (different drawing backends
may zero alpha on antialiased edges) until visually verified there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 14:31:18 +03:00
Uladzimir KarpenkaandClaude Opus 4.8 55d5bf01cf Optimize CPU waterfall/spectrum rendering
WaterfallView: drop per-frame TLazIntfImage + LoadFromIntfImage (device
bitmap recreation + full pixel-format conversion every frame). Write
pixels directly into the bitmap via BeginUpdate/ScanLine like
SpectrumView already does, with platform-native byte order (ARGB on
Cocoa, BGRA elsewhere).

Replace per-pixel float colorization with a 512-entry dB->color LUT
rebuilt only when WfLow/WfHigh/theme change beyond a small threshold.

Turn the bitmap itself into a ring buffer: each frame builds a single
new row and writes one ScanLine; scrolling is done at paint time via two
CopyRect calls. Removes both full-frame passes per frame (image memmove
+ buffer->bitmap copy), cutting per-frame cost from O(W*H) to O(W). TX
mode preserved by leaving out-of-span columns untouched in the row buf.

SpectrumView: rewrite the per-frame alpha-restore pass from a strided
per-byte loop to a sequential per-dword OR with the alpha mask
(bit-identical output, sequential memory access).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 14:21:40 +03:00
Uladzimir Karpenka 16259ed704 add script create macOS bundle app 2026-05-29 08:00:22 +03:00
Uladzimir Karpenka 2ab02d7681 Store WDSP wisdom file in app config dir alongside settings JSON 2026-05-28 22:16:57 +03:00
Uladzimir Karpenka a02d35fcce Fix macOS pixel format: use ARGB byte order in all raw ScanLine blending
On macOS Cocoa, TBitmap.pf32bit stores pixels in ARGB order
   (byte0=A, byte1=R, byte2=G, byte3=B) rather than BGRA used on
   Linux/Windows. All raw ScanLine pixel blending and channel mapping
   is now guarded with { DARWIN} to use the correct offsets.

   Also fix PortAudio library discovery on macOS: probe .dylib names
   (including Homebrew paths) before falling through to .so/.dll.
2026-05-28 21:47:54 +03:00
Uladzimir Karpenka 4f127a30ad fix macOS build 2026-05-28 15:59:49 +03:00