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>
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>
Первая команда тяжёлого класса. 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>
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>