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>
This commit is contained in:
Uladzimir Karpenka
2026-06-04 20:17:45 +03:00
co-authored by Claude Opus 4.8
parent 6edcf8bdf4
commit 908c02d08a
2 changed files with 160 additions and 164 deletions
+130 -4
View File
@@ -47,10 +47,10 @@ unit RadioController;
interface
uses
Classes, SysUtils,
Classes, SysUtils, Math,
HPSDRProtocol, HPSDRNetwork,
WDSPEngine, AudioOutput, AudioInput,
Settings, ChannelStore, FMRepeater;
Settings, ChannelStore, FMRepeater, BoardUtils;
type
// Поле, которое изменилось — для гранулярных уведомлений наружу.
@@ -110,7 +110,6 @@ type
procedure Changed(Field: TRadioField);
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
procedure PushNetworkState; // полный HP-кадр в радио (RX/TX freq+drive)
public
// ============================================================
// ПУБЛИЧНЫЕ ПОЛЯ (миграционные — имена совпадают с TMainForm).
@@ -147,6 +146,7 @@ type
FMuted: Boolean;
FDrivePercent: Integer; // драйв 0..100 % (UI-уровень, источник истины)
FDriveLevel: Byte; // вычисляемый hardware-байт (CalcDriveByte)
FLastVfoScrolled: Boolean; // последний SetVfoA прокрутил центр (для водопада)
FAtten: Integer;
FPAMaxPower: Double;
FRunning: Boolean;
@@ -216,6 +216,14 @@ type
// FM-девиация, squelch). Вызывается командами и UI-путями смены режима.
procedure ApplyModeFilter;
// Вычисляет hardware drive-байт (Thetis-формула) из текущего состояния
// (band/xvtr cal, TUN/drive%, PA max). Чистая функция над состоянием.
function CalcDriveByte: Byte;
// Полный HP-кадр в радио (RX/TX freq через трансвертор + drive + MOX).
// Вызывается командами смены частоты/центра и UI после правки drive.
procedure PushNetworkState;
// ====================================================================
// КОМАНДЫ — единый API для GUI / Web / CAT / Arduino.
// Фаза 1: обновляют поле + шлют событие; логика движка — TODO-wiring.
@@ -441,8 +449,126 @@ end;
// Команды (Фаза 1 — каркас). Полная логика переносится из TMainForm.
// =====================================================================
function TRadioController.CalcDriveByte: Byte;
// Thetis-совместимая формула:
// drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 )
// target_volts = sqrt( 10^(target_dBm/10) * 0.05 )
// target_dBm = 10*log10(pwr_W*1000) - Cal ; pwr_W = Pos/100 * FPAMaxPower
var
Cal: Double;
Pos: Integer;
XvtrActive: Boolean;
pwr_W: Double;
target_dBm: Double;
target_volts: Double;
audio_vol: Double;
begin
XvtrActive := (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled;
if XvtrActive then
Cal := FVHFBandCal[FCurrentXvtr]
else if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
Cal := FPABandCal[FCurrentBand]
else
Cal := 38.8;
if FTuning then
begin
if XvtrActive and FXvtrSettings.UseXVTRTunePower then
Pos := EnsureRange(FXvtrSettings.Entries[FCurrentXvtr].TXPower, 0, 100)
else
Pos := EnsureRange(FTXSettings.TUNLevel, 0, 100);
end
else
begin
if XvtrActive then
Pos := Round(FDrivePercent * FXvtrSettings.Entries[FCurrentXvtr].TXPower / 100.0)
else
Pos := FDrivePercent;
end;
if Pos <= 0 then begin Result := 0; Exit; end;
pwr_W := Pos / 100.0 * FPAMaxPower;
target_dBm := 10.0 * Log10(pwr_W * 1000.0) - Cal;
target_volts := Sqrt(Power(10.0, target_dBm * 0.1) * 0.05);
audio_vol := Min(target_volts / 0.8, 1.0);
Result := Min(Round(audio_vol * 1.02 * 255.0), 255);
end;
procedure TRadioController.SetVfoA(Hz: Double);
begin FVfoA := Hz; Changed(rfVfoA); { TODO wiring: ApplyVfoA } end;
// Чистое ядро перестройки VFO A: XVTR-клэмп, CTUN shift/center (с прокруткой
// центра при выходе полосы фильтра за край), детект диапазона, FM авто-RPT,
// проталкивание в сеть. Рендер (FreqDisp/спектр/band-кнопки) — в UI через
// OnControllerState(rfVfoA/rfBand). Канальную оркестрацию (авто-CTCSS,
// pre-channel drive) делает вызывающий UI-путь.
var
NewFreq: Int64;
Offset: Double;
FiltLo, FiltHi, HalfSpan: Double;
BandIdx: Integer;
begin
NewFreq := Round(Hz);
// Клэмп в пределах XVTR-band — не даём VFO уйти за FreqBegin/FreqEnd
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
begin
if NewFreq < FXvtrSettings.Entries[FCurrentXvtr].FreqBegin then
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqBegin);
if NewFreq > FXvtrSettings.Entries[FCurrentXvtr].FreqEnd then
NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqEnd);
end;
FVfoA := NewFreq;
FLastVfoScrolled := False;
if not FCTun then
begin
// CTUN OFF: DDC = VFO, сдвига нет
FCenterFreq := FVfoA;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(0.0);
end
else
begin
// CTUN ON: DDC фиксирован, демодулятор сдвигаем на FVfoA-FCenterFreq
Offset := FVfoA - FCenterFreq;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset);
// Полоса фильтра вышла за край дисплея → прокручиваем центр
HalfSpan := FSpanHz / 2;
case FMode of
0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end;
else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end;
end;
if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then
begin
if FiltHi > HalfSpan then
FCenterFreq := FVfoA - HalfSpan * 0.5
else
FCenterFreq := FVfoA + HalfSpan * 0.5;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(FVfoA - FCenterFreq);
FLastVfoScrolled := True;
end;
end;
// Детект диапазона (только HF: в XVTR клэмп держит VFO внутри band,
// FreqToBandIdx вернёт -1 для VHF/UHF).
if FCurrentXvtr < 0 then
begin
BandIdx := FreqToBandIdx(FVfoA);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin
FCurrentBand := BandIdx;
FDriveLevel := CalcDriveByte;
Changed(rfBand);
end;
end;
// Авто-MINUS в 2 м репитерном сегменте (само снимает при уходе из сегмента).
if FMode = MODE_FM then UpdateRptAutoState;
PushNetworkState;
Changed(rfVfoA);
end;
procedure TRadioController.SetVfoB(Hz: Double);
begin FVfoB := Hz; Changed(rfVfoB); { TODO wiring } end;