Move the device bring-up tail of DoConnectDevice into TRadioController,
symmetric with StopRunning:
- Relocate four pure DSP/network helpers into the controller
(BuildMicLineSelectByte, ApplyNoiseFilterButtonsToDSP,
SendDUCSpecificFromSettings, ApplyTXSettingsToDSP); external callers
(OnTXSettingsChange) now call FController.*.
- New StartRunning(SpectrumWidth): General packet, wideband/DDC config,
WDSP sample-rate sync, DSP restore (mode/volume/filter/NR/AGC), Run=1
sequence, seq reset, DDC/DUC Specific + TX chain, AGC lines. Emits
Changed(rfRunning) so the run button / MOX enable / sleep inhibit render
through OnControllerState.
DoConnectDevice's ~80-line Phase C collapses to one call; only UI bits
(FSpecView.AGCTop, status text, ResizeSpectrumPanels) stay in the wrapper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the 14 persisted display-state fields (FShow*/FSpec*/FWf*/FTXSpec*/
FWidebandFill/FSpectrumFill) from MainForm into TRadioController so a
headless device load (DoConnectDevice) can apply them without the UI. Pure
field relocation: every reader/writer now addresses FController.F*, render
still happens in MainForm against FSpecView/FWidebandView. Compiler-verified
complete; behavior preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
Первая команда тяжёлого класса. 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>
Перенос частотных хелперов (общий блокер для 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Without it Inno Setup skips files whose version matches the installed
one, so the exe was not replaced on updates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FPC's GetAppConfigDir on Windows falls back to the exe directory
if the shell API call fails, which breaks installs in Program Files.
Read %APPDATA% from the environment variable instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Program Files is read-only for normal users; GetAppConfigDir(False)
returns the proper per-user config location on all platforms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>