feat(cat): wire split/FM/CTUN/AGC-T CAT commands to controller

Реализованы команды, ранее бывшие заглушками — функционал уже был в
TRadioController, но не пробрасывался в CAT:

- Split TX: ZZSP/FT/FR + split-байт в IF (новый RadioController.SetSplit)
- FM Squelch: SQ (0–255), ZZSQ (0–100%), ZZSO (on/off)
- FM CTCSS: CT/ZZTA (on), CN (1-based)/ZZTB (0-based тон)
- FM-репитер: OF (offset), OS (направление, с трансляцией Kenwood↔RPT_*)
- FM step: ZZST (00–03)
- CTUN: ZZCN; AGC-T/RF gain: ZZAR
- ANF: ZZNT (раньше ошибочно алиас NB)
- Шаги VFO: ZZAU/ZZBP (исправлен баг — были алиасы ZZFA/ZZAP), ZZSU

TCATContext расширен callback'ами (split, squelch, CTCSS, repeater, step,
AGC-T, CTUN); CATAdapter — геттеры/сеттеры/sync с маршалингом через Invoke.
Добавлен doc/CAT_STATUS.md — полный статус CAT и план работ.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 10:35:35 +03:00
co-authored by Claude Opus 4.8
parent 7938e800f3
commit 4a92624e9b
4 changed files with 614 additions and 63 deletions
+270 -63
View File
@@ -36,6 +36,10 @@ const
MODE_CWL = 3; MODE_CWU = 4; MODE_FM = 5;
MODE_AM = 6; MODE_SAM = 7;
// FM repeater offset directions — зеркало FMRepeater.RPT_* (значения должны
// совпадать: контроллер хранит FFMRptDir в этих же кодах).
RPT_NONE = 0; RPT_MINUS = 1; RPT_PLUS = 2;
type
TCATGetDoubleFunc = function: Double of object;
TCATGetIntFunc = function: Integer of object;
@@ -81,6 +85,27 @@ type
DoTuneUp: TCATProc;
DoTuneDown: TCATProc;
DoBandByIndex: TCATSetIntProc;
// Split / FM (squelch, CTCSS, repeater, step)
GetSplit: TCATGetBoolFunc;
SetSplit: TCATSetBoolProc;
GetSquelchOn: TCATGetBoolFunc;
SetSquelchOn: TCATSetBoolProc;
GetSquelchLevel: TCATGetIntFunc; // 0..100
SetSquelchLevel: TCATSetIntProc; // 0..100
GetCTCSSOn: TCATGetBoolFunc;
SetCTCSSOn: TCATSetBoolProc;
GetCTCSSTone: TCATGetIntFunc; // index 0..37
SetCTCSSTone: TCATSetIntProc; // index 0..37
GetFMRptDir: TCATGetIntFunc; // 0=none,1=minus,2=plus
SetFMRptDir: TCATSetIntProc;
GetFMRptOffset: TCATGetDoubleFunc; // Hz
SetFMRptOffset: TCATSetDoubleProc; // Hz
GetFMStep: TCATGetIntFunc; // index 0..3 (6.25/12.5/20/25 kHz)
SetFMStep: TCATSetIntProc; // index 0..3
GetAGCTop: TCATGetIntFunc; // AGC-T порог, dB
SetAGCTop: TCATSetIntProc; // AGC-T порог, dB
GetCTun: TCATGetBoolFunc; // Click-tune (CTUN) вкл/выкл
SetCTun: TCATSetBoolProc;
end;
TCATEngine = class
@@ -111,6 +136,17 @@ type
function SafeGetRunning: Boolean;
function SafeGetSMeter: Double;
function SafeGetBand: Integer;
function SafeGetSplit: Boolean;
function SafeGetSquelchOn: Boolean;
function SafeGetSquelchLevel: Integer;
function SafeGetCTCSSOn: Boolean;
function SafeGetCTCSSTone: Integer;
function SafeGetFMRptDir: Integer;
function SafeGetFMRptOffset: Double;
function SafeGetFMStep: Integer;
function SafeGetAGCTop: Integer;
function SafeGetCTun: Boolean;
function StepIdxToHz(Idx: Integer): Double;
// command implementations
function CmdAG(const s: string): string;
@@ -165,8 +201,10 @@ type
function ZZAR(const s: string): string;
function ZZAS(const s: string): string;
function ZZAT(const s: string): string;
function ZZAU(const s: string): string;
function ZZBA: string;
function ZZBD: string;
function ZZBP(const s: string): string;
function ZZBE(const s: string): string;
function ZZBI(const s: string): string;
function ZZBM(const s: string): string;
@@ -241,7 +279,7 @@ type
function ZZSP(const s: string): string;
function ZZSQ(const s: string): string;
function ZZSR(const s: string): string;
function ZZST: string;
function ZZST(const s: string): string;
function ZZSU: string;
function ZZTA(const s: string): string;
function ZZTB(const s: string): string;
@@ -433,6 +471,55 @@ function TCATEngine.SafeGetBand: Integer;
begin
if Assigned(FCtx.GetCurrentBand) then Result := FCtx.GetCurrentBand else Result := 5;
end;
function TCATEngine.SafeGetSplit: Boolean;
begin
if Assigned(FCtx.GetSplit) then Result := FCtx.GetSplit else Result := False;
end;
function TCATEngine.SafeGetSquelchOn: Boolean;
begin
if Assigned(FCtx.GetSquelchOn) then Result := FCtx.GetSquelchOn else Result := False;
end;
function TCATEngine.SafeGetSquelchLevel: Integer;
begin
if Assigned(FCtx.GetSquelchLevel) then Result := FCtx.GetSquelchLevel else Result := 0;
end;
function TCATEngine.SafeGetCTCSSOn: Boolean;
begin
if Assigned(FCtx.GetCTCSSOn) then Result := FCtx.GetCTCSSOn else Result := False;
end;
function TCATEngine.SafeGetCTCSSTone: Integer;
begin
if Assigned(FCtx.GetCTCSSTone) then Result := FCtx.GetCTCSSTone else Result := 0;
end;
function TCATEngine.SafeGetFMRptDir: Integer;
begin
if Assigned(FCtx.GetFMRptDir) then Result := FCtx.GetFMRptDir else Result := 0;
end;
function TCATEngine.SafeGetFMRptOffset: Double;
begin
if Assigned(FCtx.GetFMRptOffset) then Result := FCtx.GetFMRptOffset else Result := 0;
end;
function TCATEngine.SafeGetFMStep: Integer;
begin
if Assigned(FCtx.GetFMStep) then Result := FCtx.GetFMStep else Result := 0;
end;
function TCATEngine.SafeGetAGCTop: Integer;
begin
if Assigned(FCtx.GetAGCTop) then Result := FCtx.GetAGCTop else Result := 0;
end;
function TCATEngine.SafeGetCTun: Boolean;
begin
if Assigned(FCtx.GetCTun) then Result := FCtx.GetCTun else Result := False;
end;
function TCATEngine.StepIdxToHz(Idx: Integer): Double;
// Шаг настройки по индексу (0..14) — зеркало Thetis Step2Freq (в Гц).
const Steps: array[0..14] of Double = (
1, 10, 25, 50, 100, 250, 500, 1000, 5000, 9000,
10000, 100000, 250000, 500000, 1000000);
begin
if (Idx < 0) or (Idx > 14) then Result := 0
else Result := Steps[Idx];
end;
{ ── Constructor / Destructor ─────────────────────────────────────────────── }
@@ -570,23 +657,27 @@ begin
end;
function TCATEngine.CmdCN(const s: string): string;
// CN: CTCSS tone — STUB: FM sub-audible tones not implemented
// CN: CTCSS tone number (2 digits, 1-based 01..38 по таблице CTCSS_TONES).
var idx: Integer;
begin
if Length(s) = 2 then
Result := ''
else if Length(s) = 0 then
Result := '00'
if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 1) - 1, 0, 37);
if Assigned(FCtx.SetCTCSSTone) then FCtx.SetCTCSSTone(idx);
Result := '';
end else if Length(s) = 0 then
Result := Pad(EnsureRange(SafeGetCTCSSTone, 0, 37) + 1, 2)
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdCT(const s: string): string;
// CT: CTCSS enable — STUB: not implemented
// CT: CTCSS enable (1=on, 0=off) для FM-передачи.
begin
if Length(s) = 1 then
Result := ''
else if Length(s) = 0 then
Result := '0'
if Length(s) = 1 then begin
if Assigned(FCtx.SetCTCSSOn) then FCtx.SetCTCSSOn(s = '1');
Result := '';
end else if Length(s) = 0 then
Result := B2C(SafeGetCTCSSOn)
else
Result := CAT_ERROR;
end;
@@ -622,13 +713,20 @@ begin
end;
function TCATEngine.CmdFR(const s: string): string;
// FR: receive VFO — always VFO A, dummy for compat
// FR: receive VFO (0=VFO A, 1=VFO B) — выбирает активный приёмный VFO.
begin
if Length(s) = 1 then
Result := ''
else if Length(s) = 0 then
Result := '0'
else
if Length(s) = 1 then begin
if (s = '0') or (s = '1') then begin
if Assigned(FCtx.SetActiveVfo) then FCtx.SetActiveVfo(StrToIntDef(s, 0));
Result := '';
end else
Result := CAT_ERROR;
end else if Length(s) = 0 then begin
if Assigned(FCtx.GetActiveVfo) then
Result := IntToStr(EnsureRange(FCtx.GetActiveVfo, 0, 1))
else
Result := '0';
end else
Result := CAT_ERROR;
end;
@@ -677,14 +775,14 @@ var
m: Integer;
begin
freq := FreqToStr(SafeGetVfoA);
step := '0000'; // step size — STUB: no step control
step := '0000'; // step size — FM step не маппится на Kenwood-формат IF (см. ZZST)
incr := '+00000'; // RIT/XIT value — STUB: no RIT/XIT
rit := '0';
xit := '0';
m := ModeToK(SafeGetMode);
mode := IntToStr(m);
tx := B2C(SafeGetTX);
split := '0'; // STUB: no split
split := B2C(SafeGetSplit);
Result := freq + step + incr + rit + xit + '000' + tx + mode + '0' + '0' + split + '0000';
end;
@@ -765,19 +863,38 @@ begin
end;
function TCATEngine.CmdOF(const s: string): string;
// OF: FM repeater offset frequency — STUB: no FM repeater
// OF: FM repeater offset frequency (9 digits, Hz).
begin
if Length(s) = 9 then Result := ''
else if Length(s) = 0 then Result := '000000000'
else Result := CAT_ERROR;
if Length(s) = 9 then begin
if Assigned(FCtx.SetFMRptOffset) then FCtx.SetFMRptOffset(StrToIntDef(s, 0));
Result := '';
end else if Length(s) = 0 then
Result := Pad(Round(SafeGetFMRptOffset), 9)
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdOS(const s: string): string;
// OS: repeater offset direction — STUB
// OS: repeater offset direction (Kenwood: 0=simplex, 1=plus, 2=minus).
// Внутренние константы: RPT_NONE=0, RPT_MINUS=1, RPT_PLUS=2 → требуется трансляция.
var dir: Integer;
begin
if Length(s) = 1 then Result := ''
else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
if Length(s) = 1 then begin
case StrToIntDef(s, 0) of
1: dir := RPT_PLUS;
2: dir := RPT_MINUS;
else dir := RPT_NONE;
end;
if Assigned(FCtx.SetFMRptDir) then FCtx.SetFMRptDir(dir);
Result := '';
end else if Length(s) = 0 then begin
case SafeGetFMRptDir of
RPT_PLUS: Result := '1';
RPT_MINUS: Result := '2';
else Result := '0';
end;
end else
Result := CAT_ERROR;
end;
function TCATEngine.CmdPC(const s: string): string;
@@ -888,11 +1005,21 @@ begin
end;
function TCATEngine.CmdSQ(const s: string): string;
// SQ: squelch — STUB: no squelch control
// SQ: squelch. SET: <sel><lll> (sel=0/1 RX, lll=000255). GET: <sel> → <sel><lll>.
// Шкала Kenwood 0–255 маппится на внутренний уровень 0–100 %.
var lvl255, lvl100: Integer;
begin
if Length(s) = 4 then Result := ''
else if Length(s) = 1 then Result := '0000'
else Result := CAT_ERROR;
if Length(s) = 4 then begin
lvl255 := EnsureRange(StrToIntDef(Copy(s, 2, 3), 0), 0, 255);
lvl100 := Round(lvl255 * 100 / 255);
if Assigned(FCtx.SetSquelchLevel) then FCtx.SetSquelchLevel(lvl100);
if Assigned(FCtx.SetSquelchOn) then FCtx.SetSquelchOn(lvl100 > 0);
Result := '';
end else if Length(s) = 1 then begin
lvl255 := Round(EnsureRange(SafeGetSquelchLevel, 0, 100) * 255 / 100);
Result := s + Pad(lvl255, 3);
end else
Result := CAT_ERROR;
end;
function TCATEngine.CmdTX(const s: string): string;
@@ -936,7 +1063,7 @@ begin
else if ext = 'AR' then ans := ZZAR(s)
else if ext = 'AS' then ans := ZZAS(s)
else if ext = 'AT' then ans := ZZAT(s)
else if ext = 'AU' then ans := ZZFA(s) // alias ZZFA
else if ext = 'AU' then ans := ZZAU(s)
else if ext = 'AY' then ans := '' // STUB: APF type
else if ext = 'BA' then ans := ZZBA
else if ext = 'BB' then ans := '' // STUB: band button state
@@ -946,7 +1073,7 @@ begin
else if ext = 'BG' then ans := '' // STUB: sub-RX gain
else if ext = 'BI' then ans := ZZBI(s)
else if ext = 'BM' then ans := ZZBM(s)
else if ext = 'BP' then ans := ZZAP(s) // alias ZZAP
else if ext = 'BP' then ans := ZZBP(s)
else if ext = 'BR' then ans := '' // STUB: sub-RX enable
else if ext = 'BS' then ans := ZZBS(s)
else if ext = 'BT' then ans := '' // STUB: band TX
@@ -1130,7 +1257,7 @@ begin
else if ext = 'SQ' then ans := ZZSQ(s)
else if ext = 'SR' then ans := ZZSR(s)
else if ext = 'SS' then ans := '' // STUB: sub-RX
else if ext = 'ST' then begin ZZST; ans := ''; end
else if ext = 'ST' then ans := ZZST(s)
else if ext = 'SU' then begin ZZSU; ans := ''; end
else if ext = 'SV' then ans := '' // STUB: spectrum value
else if ext = 'SW' then ans := '' // STUB: spectrum write
@@ -1299,11 +1426,44 @@ function TCATEngine.ZZAP(const s: string): string;
begin Result := ZZPC(s); end;
function TCATEngine.ZZAR(const s: string): string;
// ZZAR: RIT enable — STUB: no RIT
// ZZAR: RX1 AGC-T порог (RF gain). Формат: знак + 3 цифры (напр. "+020", "+120").
// Контроллер клампит в свой диапазон (20..120 dB).
var v: Integer;
begin
if Length(s) = 1 then Result := ''
else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
if Length(s) = 4 then begin
v := StrToIntDef(s, 0); // StrToIntDef понимает ведущий +/-
if Assigned(FCtx.SetAGCTop) then FCtx.SetAGCTop(v);
Result := '';
end else if Length(s) = 0 then begin
v := SafeGetAGCTop;
if v >= 0 then Result := '+' + Pad(v, 3)
else Result := '-' + Pad(-v, 3);
end else
Result := CAT_ERROR;
end;
function TCATEngine.ZZAU(const s: string): string;
// ZZAU: сдвиг VFO A вверх на один шаг с индексом nn (00..14, см. StepIdxToHz).
var idx: Integer;
begin
if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 14);
if Assigned(FCtx.SetVfoA) then FCtx.SetVfoA(SafeGetVfoA + StepIdxToHz(idx));
Result := '';
end else
Result := CAT_ERROR;
end;
function TCATEngine.ZZBP(const s: string): string;
// ZZBP: сдвиг VFO B вверх на один шаг с индексом nn (00..14, см. StepIdxToHz).
var idx: Integer;
begin
if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 14);
if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(SafeGetVfoB + StepIdxToHz(idx));
Result := '';
end else
Result := CAT_ERROR;
end;
function TCATEngine.ZZAS(const s: string): string;
@@ -1384,9 +1544,13 @@ begin
end;
function TCATEngine.ZZCN(const s: string): string;
// ZZCN: compressor on — STUB: TX DSP not implemented
// ZZCN: CTUN (click-tune) для RX1 вкл/выкл.
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0'
if Length(s) = 1 then begin
if Assigned(FCtx.SetCTun) then FCtx.SetCTun(s = '1');
Result := '';
end else if Length(s) = 0 then
Result := B2C(SafeGetCTun)
else Result := CAT_ERROR;
end;
@@ -1712,8 +1876,15 @@ begin
end;
function TCATEngine.ZZNT(const s: string): string;
// ZZNT: noise blanker type — STUB: maps to NB mode
begin Result := ZZNB(s); end;
// ZZNT: ANF (auto-notch) вкл/выкл.
begin
if Length(s) = 1 then begin
if Assigned(FCtx.SetANFEnabled) then FCtx.SetANFEnabled(s = '1');
Result := '';
end else if Length(s) = 0 then
Result := B2C(SafeGetANF)
else Result := CAT_ERROR;
end;
function TCATEngine.ZZOA(const s: string): string;
// ZZOA: RX1 output level — alias ZZAG
@@ -1842,28 +2013,43 @@ begin
end;
function TCATEngine.ZZSO(const s: string): string;
// ZZSO: sub-RX on — STUB: no RX2
// ZZSO: squelch on/off (расширенный статус шумоподавителя). 0=off, иначе on.
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0'
if Length(s) = 1 then begin
if Assigned(FCtx.SetSquelchOn) then FCtx.SetSquelchOn(s <> '0');
Result := '';
end else if Length(s) = 0 then
Result := B2C(SafeGetSquelchOn)
else Result := CAT_ERROR;
end;
function TCATEngine.ZZSP(const s: string): string;
// ZZSP: split mode — STUB: split VFO not yet implemented in ewsdr
// ZZSP: split mode — 1=TX на VFO-B, 0=TX на VFO-A. TX-частоту радио берёт из
// ActiveTXFreqHz (учитывает split), поэтому достаточно выставить флаг.
begin
if Length(s) = 1 then
Result := '' // accepted, ignored — STUB: split VFO requires separate TX freq handling
else if Length(s) = 0 then
Result := '0'
if Length(s) = 1 then begin
if Assigned(FCtx.SetSplit) then FCtx.SetSplit(s = '1');
Result := '';
end else if Length(s) = 0 then
Result := B2C(SafeGetSplit)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZSQ(const s: string): string;
// ZZSQ: squelch — STUB
// ZZSQ: squelch threshold (3 digits, 000100 %). Уровень>0 включает шумоподавитель,
// 000 — открывает (выключает). Маппится на FM-squelch контроллера.
var v: Integer;
begin
if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '000'
else Result := CAT_ERROR;
if Length(s) = 3 then begin
v := EnsureRange(StrToIntDef(s, 0), 0, 100);
if Assigned(FCtx.SetSquelchLevel) then FCtx.SetSquelchLevel(v);
if Assigned(FCtx.SetSquelchOn) then FCtx.SetSquelchOn(v > 0);
Result := '';
end else if Length(s) = 0 then
Result := Pad(SafeGetSquelchLevel, 3)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZSR(const s: string): string;
@@ -1874,26 +2060,47 @@ begin
else Result := CAT_ERROR;
end;
function TCATEngine.ZZST: string;
// ZZST: tuning step — STUB: returns 0000 (1 Hz step)
begin Result := ''; end;
function TCATEngine.ZZST(const s: string): string;
// ZZST: FM tuning step index (2 digits): 00=6.25k, 01=12.5k, 02=20k, 03=25k.
begin
if Length(s) = 2 then begin
if Assigned(FCtx.SetFMStep) then FCtx.SetFMStep(EnsureRange(StrToIntDef(s, 0), 0, 3));
Result := '';
end else if Length(s) = 0 then
Result := Pad(EnsureRange(SafeGetFMStep, 0, 3), 2)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZSU: string;
// ZZSU: sub-RX update — STUB
begin Result := ''; end;
// ZZSU: шаг настройки вверх (как колесо мыши) — сдвигает активный VFO вверх.
begin
if Assigned(FCtx.DoTuneUp) then FCtx.DoTuneUp;
Result := '';
end;
function TCATEngine.ZZTA(const s: string): string;
// ZZTA: CTCSS on/off — STUB: FM CTCSS not implemented
// ZZTA: CTCSS on/off (1=on, 0=off) для FM-передачи.
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
if Length(s) = 1 then begin
if Assigned(FCtx.SetCTCSSOn) then FCtx.SetCTCSSOn(s = '1');
Result := '';
end else if Length(s) = 0 then
Result := B2C(SafeGetCTCSSOn)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZTB(const s: string): string;
// ZZTB: CTCSS tone frequency index — STUB
// ZZTB: CTCSS tone index (2 digits, 0-based 00..37 в таблице CTCSS_TONES).
begin
if Length(s) = 2 then Result := '' else if Length(s) = 0 then Result := '01'
else Result := CAT_ERROR;
if Length(s) = 2 then begin
if Assigned(FCtx.SetCTCSSTone) then FCtx.SetCTCSSTone(EnsureRange(StrToIntDef(s, 0), 0, 37));
Result := '';
end else if Length(s) = 0 then
Result := Pad(EnsureRange(SafeGetCTCSSTone, 0, 37), 2)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZTX(const s: string): string;