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
+121
View File
@@ -52,6 +52,16 @@ type
function GetRunning: Boolean;
function GetSMeter: Double;
function GetBand: Integer;
function GetSplit: Boolean;
function GetSquelchOn: Boolean;
function GetSquelchLevel: Integer;
function GetCTCSSOn: Boolean;
function GetCTCSSTone: Integer;
function GetFMRptDir: Integer;
function GetFMRptOffset: Double;
function GetFMStep: Integer;
function GetAGCTop: Integer;
function GetCTun: Boolean;
// ── Сеттеры/команды (из CAT-потока → Invoke в поток контроллера) ──
procedure SetVfoA(V: Double);
@@ -72,6 +82,16 @@ type
procedure DoTuneUp;
procedure DoTuneDown;
procedure DoBandByIndex(Idx: Integer);
procedure SetSplit(V: Boolean);
procedure SetSquelchOn(V: Boolean);
procedure SetSquelchLevel(V: Integer);
procedure SetCTCSSOn(V: Boolean);
procedure SetCTCSSTone(V: Integer);
procedure SetFMRptDir(V: Integer);
procedure SetFMRptOffset(V: Double);
procedure SetFMStep(V: Integer);
procedure SetAGCTop(V: Integer);
procedure SetCTun(V: Boolean);
// ── Sync-методы (выполняются в потоке контроллера) ──
procedure SyncVfoA;
@@ -92,6 +112,16 @@ type
procedure SyncTuneUp;
procedure SyncTuneDown;
procedure SyncBandByIndex;
procedure SyncSplit;
procedure SyncSquelchOn;
procedure SyncSquelchLevel;
procedure SyncCTCSSOn;
procedure SyncCTCSSTone;
procedure SyncFMRptDir;
procedure SyncFMRptOffset;
procedure SyncFMStep;
procedure SyncAGCTop;
procedure SyncCTun;
public
constructor Create(AController: TRadioController);
destructor Destroy; override;
@@ -144,6 +174,26 @@ begin
Ctx.DoTuneUp := @DoTuneUp;
Ctx.DoTuneDown := @DoTuneDown;
Ctx.DoBandByIndex := @DoBandByIndex;
Ctx.GetSplit := @GetSplit;
Ctx.SetSplit := @SetSplit;
Ctx.GetSquelchOn := @GetSquelchOn;
Ctx.SetSquelchOn := @SetSquelchOn;
Ctx.GetSquelchLevel := @GetSquelchLevel;
Ctx.SetSquelchLevel := @SetSquelchLevel;
Ctx.GetCTCSSOn := @GetCTCSSOn;
Ctx.SetCTCSSOn := @SetCTCSSOn;
Ctx.GetCTCSSTone := @GetCTCSSTone;
Ctx.SetCTCSSTone := @SetCTCSSTone;
Ctx.GetFMRptDir := @GetFMRptDir;
Ctx.SetFMRptDir := @SetFMRptDir;
Ctx.GetFMRptOffset := @GetFMRptOffset;
Ctx.SetFMRptOffset := @SetFMRptOffset;
Ctx.GetFMStep := @GetFMStep;
Ctx.SetFMStep := @SetFMStep;
Ctx.GetAGCTop := @GetAGCTop;
Ctx.SetAGCTop := @SetAGCTop;
Ctx.GetCTun := @GetCTun;
Ctx.SetCTun := @SetCTun;
FEngine := TCATEngine.Create(Ctx);
FSerial := TCATSerialManager.Create(FEngine);
@@ -203,6 +253,16 @@ function TCATAdapter.GetTX: Boolean; begin Result := FController.FTransm
function TCATAdapter.GetRunning: Boolean; begin Result := FController.FRunning; end;
function TCATAdapter.GetSMeter: Double; begin Result := FController.FLastSMeter; end;
function TCATAdapter.GetBand: Integer; begin Result := FController.FCurrentBand; end;
function TCATAdapter.GetSplit: Boolean; begin Result := FController.FSplitTxB; end;
function TCATAdapter.GetSquelchOn: Boolean; begin Result := FController.FFMSQOn; end;
function TCATAdapter.GetSquelchLevel: Integer; begin Result := FController.FFMSQLevel; end;
function TCATAdapter.GetCTCSSOn: Boolean; begin Result := FController.FFMCTCSSOn; end;
function TCATAdapter.GetCTCSSTone: Integer; begin Result := FController.FFMCTCSSToneIdx; end;
function TCATAdapter.GetFMRptDir: Integer; begin Result := FController.FFMRptDir; end;
function TCATAdapter.GetFMRptOffset: Double; begin Result := FController.FFMRptOffsetHz; end;
function TCATAdapter.GetFMStep: Integer; begin Result := FController.FFMStepIdx; end;
function TCATAdapter.GetAGCTop: Integer; begin Result := FController.FAGCTop; end;
function TCATAdapter.GetCTun: Boolean; begin Result := FController.FCTun; end;
// ── Сеттеры (маршалинг) ──────────────────────────────────────────────────────
procedure TCATAdapter.SetVfoA(V: Double);
@@ -321,4 +381,65 @@ begin FSyncInt := Idx; FController.Invoke(@SyncBandByIndex); end;
procedure TCATAdapter.SyncBandByIndex;
begin FController.SetBand(FSyncInt); end;
// ── Split / FM (squelch, CTCSS, repeater, step) ───────────────────────────────
procedure TCATAdapter.SetSplit(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncSplit); end;
procedure TCATAdapter.SyncSplit;
begin FController.SetSplit(FSyncBool); end;
procedure TCATAdapter.SetSquelchOn(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncSquelchOn); end;
procedure TCATAdapter.SyncSquelchOn;
begin FController.SetFMSquelch(FSyncBool); end;
procedure TCATAdapter.SetSquelchLevel(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncSquelchLevel); end;
procedure TCATAdapter.SyncSquelchLevel;
begin FController.SetFMSquelchLevel(FSyncInt); end;
procedure TCATAdapter.SetCTCSSOn(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncCTCSSOn); end;
procedure TCATAdapter.SyncCTCSSOn;
begin FController.SetFMCTCSS(FSyncBool); end;
procedure TCATAdapter.SetCTCSSTone(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncCTCSSTone); end;
procedure TCATAdapter.SyncCTCSSTone;
begin FController.SetFMCTCSSTone(FSyncInt); end;
procedure TCATAdapter.SetFMRptDir(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncFMRptDir); end;
procedure TCATAdapter.SyncFMRptDir;
begin FController.SetFMRpt(FSyncInt); end;
procedure TCATAdapter.SetFMRptOffset(V: Double);
begin FSyncFreq := V; FController.Invoke(@SyncFMRptOffset); end;
procedure TCATAdapter.SyncFMRptOffset;
begin FController.SetFMRptOffset(FSyncFreq); end;
procedure TCATAdapter.SetFMStep(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncFMStep); end;
procedure TCATAdapter.SyncFMStep;
begin FController.SetFMStep(FSyncInt); end;
procedure TCATAdapter.SetAGCTop(V: Integer);
begin FSyncInt := V; FController.Invoke(@SyncAGCTop); end;
procedure TCATAdapter.SyncAGCTop;
begin FController.SetAGCTop(FSyncInt); end;
procedure TCATAdapter.SetCTun(V: Boolean);
begin FSyncBool := V; FController.Invoke(@SyncCTun); end;
procedure TCATAdapter.SyncCTun;
begin FController.SetCTun(FSyncBool); end;
end.
+269 -62
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'
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;
+12
View File
@@ -513,6 +513,7 @@ type
// Передача
procedure SetMOX(On_: Boolean);
procedure SetTune(On_: Boolean); // TUN
procedure SetSplit(TxOnB: Boolean); // split: TX на VFO-B (иначе VFO-A)
// FM
procedure SetFMStep(Idx: Integer);
@@ -2686,6 +2687,17 @@ begin
Changed(rfFMSQLevel);
end;
procedure TRadioController.SetSplit(TxOnB: Boolean);
// Split: TX-VFO = B (TxOnB) либо A. TX-частота берётся из ActiveTXFreqHz
// (он смотрит на FSplitTxB), поэтому достаточно протолкнуть HP-кадр в сеть.
// Рендер VFO-дисплеев и подсветки TX-A/TX-B кнопок — через rfActiveVfo.
begin
if FSplitTxB = TxOnB then Exit;
FSplitTxB := TxOnB;
PushNetworkState;
Changed(rfActiveVfo);
end;
procedure TRadioController.SetFMRpt(Dir: Integer);
begin
FFMRptDir := Dir;
+211
View File
@@ -0,0 +1,211 @@
# CAT-система EWSDR — статус реализации
Ветка разработки: `feature/cat-commands-expansion`.
Дата последнего обновления: 2026-06-26.
Документ описывает текущее состояние CAT-подсистемы EWSDR: архитектуру,
полный перечень реализованных команд, осознанные заглушки и план дальнейших
работ. За эталон протокола взят **Thetis / PowerSDR** (`CATParser.cs`,
`CATCommands.cs`, `CATStructs.xml`) — диалект Kenwood TS-2000 + расширения `ZZ*`.
---
## 1. Архитектура
```
CAT-транспорты ──► TCATEngine (парсер протокола) ──► TCATContext ──► TCATAdapter ──► TRadioController
serial (4×) CATEngine.pas (callbacks) CATAdapter.pas (ядро)
TCP-сервер разбор/формат команд мост, маршалинг
```
| Файл | Назначение |
|---|---|
| `CATEngine.pas` (~2270 строк) | Движок протокола: разбор `<PREFIX><SUFFIX>;`, формирование ответов, реализации всех команд Kenwood + `ZZ*`. Зависит только от RTL. |
| `CATAdapter.pas` (~445 строк) | Мост к `TRadioController`. Строит `TCATContext`: геттеры читают поля контроллера напрямую (CAT-поток, read-only), сеттеры/команды маршалятся в поток контроллера через `FController.Invoke`. Владеет движком + транспортами. |
| `CATSerial.pas` (~313 строк) | Менеджер до 4 последовательных портов (виртуальные COM-пары для rigctld/N1MM/WSJT-X). |
| `CATTcp.pas` (~507 строк) | TCP-сервер (Thetis-совместимый), управление клиентами по GUID (`ZZGA`/`ZZGR`). |
### 1.1 Принципы
- **CAT — равноправный клиент контроллера** наряду с `WebAdapter` и GUI. Никаких
обращений к `MainForm`. Только `TRadioController`.
- **Потокобезопасность.** Геттеры читают поля контроллера напрямую (атомарное
чтение в CAT-потоке). Сеттеры пишут параметр в scratch-поля
(`FSyncFreq/FSyncInt/FSyncBool`) и зовут `FController.Invoke(@SyncXxx)`
GUI исполняет через `TThread.Synchronize`, демон — своей очередью.
- **`TCATContext`** — плоская запись callback'ов (`Get*`/`Set*`/`Do*`). Чтобы
пробросить новую возможность контроллера в CAT, достаточно: (1) добавить
callback в запись, (2) реализовать в адаптере геттер/сеттер/sync, (3)
зарегистрировать в `TCATAdapter.Create`, (4) переписать тело команды в движке.
- **Формат команд:**
```
SET: <PREFIX><SUFFIX>; (suffix = значение)
GET: <PREFIX>; (suffix пустой)
Resp: <PREFIX><VALUE>;
Err: ?; (неверный формат) | E; (ошибка выполнения)
```
Расширенные `ZZ*` дублируют Kenwood-команды и добавляют то, чего в Kenwood нет.
### 1.2 Статистика покрытия (ZZ-команды)
| | Кол-во |
|---|---|
| Всего `ZZ*` в Thetis | 315 |
| Реально реализовано в EWSDR | **98** |
| Заглушки (отвечают валидным dummy, функции нет) | ~211 |
| Совсем не обрабатываются (вернут `?;`) | 7 |
Большинство заглушек закрыть **нечем** — в EWSDR физически отсутствует
соответствующий тракт (см. §4).
---
## 2. Что сделано
### 2.1 Реализованные команды Kenwood (одиночный префикс)
| Команда | Функция | Маппинг на контроллер |
|---|---|---|
| `FA` / `FB` | VFO A / B частота (11 цифр, Гц) | `Set/GetVfoA`, `Set/GetVfoB` |
| `FR` | приёмный VFO (0/1) | `Set/GetActiveVfo` |
| `FT` | передающий VFO / split | → `ZZSP` |
| `FW` / `SH` / `SL` | ширина фильтра (индекс пресета) | `Set/GetFilterIdx` |
| `MD` | режим (Kenwood-код) | `Set/GetMode` |
| `GT` | режим AGC (0=FAST…4=OFF) | `Set/GetAGCMode` |
| `AG` | громкость AF | `Set/GetVolume` |
| `PC` | мощность TX 0–100 % | `Set/GetDriveLevel` |
| `NB` | шумоподавитель (NB) | `Set/GetNBMode` |
| `NT` | автонотч (ANF) | `Set/GetANFEnabled` |
| `SM` | S-метр (шкала Kenwood 00000030) | `GetSMeter` |
| `IF` | составной статус (37 байт) | freq/mode/tx/split |
| `ID` | идентификатор (TS-2000 = 019) | const |
| `PS` | питание/состояние | `GetRunning` |
| `TX` / `RX` | передача / приём | `SetTransmitting` |
| `BU` / `BD` | смена диапазона ±1 | `DoBandUp/Down` |
| `UP` / `DN` / `RU` / `RD` | шаг настройки ±1 | `DoTuneUp/Down` |
| `SQ` | шумоподавитель FM (шкала 0–255) | `Set/GetSquelchLevel`+`On` |
| `CN` / `CT` | CTCSS тон / вкл | `Set/GetCTCSSTone`, `…On` |
| `OF` / `OS` | репитер: offset / направление | `Set/GetFMRptOffset`, `…Dir` |
### 2.2 Реализованные расширенные `ZZ*` (выборочно, наиболее значимые)
VFO/режим/фильтр: `ZZFA ZZFB ZZFI ZZFL ZZFH ZZFM ZZMD ZZFT ZZSP`,
диапазоны: `ZZBS ZZBU ZZBD ZZBA ZZBI ZZBM`,
AGC/громкость/мощность: `ZZGT ZZAG ZZPC ZZPD ZZAR`,
шумодав/нотч: `ZZNA ZZNB ZZNR ZZNS ZZNT ZZNL ZZNN`,
FM: `ZZSQ ZZSO ZZTA ZZTB ZZST`, CTUN: `ZZCN`,
S-метр/телеметрия: `ZZSM`, шаги VFO: `ZZAU ZZBP ZZSU ZZSA ZZSB`,
передача: `ZZTX ZZRX`, сервис: `ZZID ZZRV ZZGA ZZGR`.
### 2.3 Сделано в ветке `feature/cat-commands-expansion`
Все команды ниже **были заглушками** и стали рабочими — функционал в
`TRadioController` уже существовал, но не был проброшен в CAT.
**Группа 1 — Split TX**
- `ZZSP`, `FT` — split (TX на VFO-B), `FR` — выбор приёмного VFO,
split-байт в составном `IF`.
- В `TRadioController` добавлен публичный метод `SetSplit(TxOnB)`
(флаг `FSplitTxB` + `PushNetworkState` + `Changed(rfActiveVfo)`).
TX-частоту радио берёт из `ActiveTXFreqHz` (учитывает split).
**Группа 2 — FM Squelch**
- `SQ` (шкала Kenwood 0255), `ZZSQ` (0100 %), `ZZSO` (вкл/выкл).
- Маппинг: `SetFMSquelch` / `SetFMSquelchLevel`. Уровень>0 включает
шумоподавитель.
**Группа 3 — FM CTCSS**
- `CT`/`ZZTA` (вкл/выкл), `CN` (1-based тон 0138), `ZZTB` (0-based 0037).
- Маппинг: `SetFMCTCSS` / `SetFMCTCSSTone`, таблица `CTCSS_TONES[38]`.
**Группа 4 — FM-репитер**
- `OF` (offset, 9 цифр Гц), `OS` (направление).
- Трансляция направлений Kenwood (0=simplex,1=+,2=) ↔ внутренние
`RPT_NONE/MINUS/PLUS`. Маппинг: `SetFMRpt` / `SetFMRptOffset`.
**Группа 5 — FM tuning step**
- `ZZST` (индекс 0003: 6.25/12.5/20/25 кГц). Маппинг: `SetFMStep`.
**Доп. команды (вторая итерация)**
| Команда | Что | Примечание |
|---|---|---|
| `ZZSO` | Squelch on/off | завершает группу squelch |
| `ZZNT` | ANF on/off | раньше ошибочно был алиасом NB |
| `ZZCN` | CTUN (click-tune) RX1 | новый callback `Get/SetCTun``SetCTun` |
| `ZZAR` | RX1 AGC-T / RF gain (`±nnn`) | новый callback `Get/SetAGCTop``SetAGCTop` (кламп 20120 dB) |
| `ZZAU`/`ZZBP` | сдвиг VFO A/B вверх на шаг `nn` (0014) | **исправлен баг**: были заалиашены на `ZZFA`/`ZZAP` |
| `ZZSU` | шаг активного VFO вверх | `DoTuneUp` |
Шкала шагов `ZZAU/ZZBP``StepIdxToHz` (зеркало Thetis `Step2Freq`):
1 Гц, 10, 25, 50, 100, 250, 500, 1к, 5к, 9к, 10к, 100к, 250к, 500к, 1 МГц.
**Новые callback'и в `TCATContext`** (этой ветки): split, squelch on/level,
CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN.
---
## 3. Что нужно сделать (актуально / есть чем закрыть)
Эти команды — заглушки, но соответствующая функция в EWSDR **уже есть** или
легко добавляется. Приоритетные кандидаты:
| Команда(ы) | Что | Чем закрыть | Сложность |
|---|---|---|---|
| `ZZCU` | загрузка CPU (read) | телеметрия процесса | низкая |
| `ZZMY` / `ZZQR` / `ZZQS` / `ZZQM` | память каналов (запись/чтение/quick) | `ChannelStore` уже есть | средняя |
| `ZZFD` | FM-девиация (radio button) | нужен публичный `SetFMDeviation` (сейчас только поле `FFMDeviation`) | низкая |
| `ZZDM` / `ZZDA` / `ZZPO` / `ZZDY` | режим/усреднение/пик/порог спектра | UI-параметры спектра | средняя |
| `ZZAS` | RX2 AGC-T | — только при появлении RX2 | — |
> Прежде чем реализовывать новую команду, сверяйся с форматом поля в
> `Thetis/.../CAT/CATStructs.xml` (`<nsetparms>` / `<ngetparms>`) и телом
> метода в `CATCommands.cs`, чтобы ширина/знак значения совпадали.
### 3.1 Совсем не обрабатываются (вернут `?;`)
`ZZCU` (CPU), `ZZDY` (DX threshold), `ZZPK`/`ZZPL` (речевой компрессор),
`ZZQA`/`ZZQB` (Quick Play/Rec), `ZZVF` (VAC stereo).
Из них реалистично добавить только `ZZCU` и (частично) `ZZDY`.
---
## 4. Осознанные заглушки (закрывать нечем)
Команды отвечают валидным dummy-значением для совместимости с управляющим ПО
(rigctld, N1MM, WSJT-X, Log4OM), но функции в EWSDR нет. Это ~70 % всех заглушек.
| Область | Команды (примеры) | Причина |
|---|---|---|
| **Второй приёмник RX2 / sub-RX** | `ZZAB ZZAS ZZBA ZZMB ZZOB`, ext `BF BG BR DO DQ DR OF OG OH OL SG SH SS` | один приёмник |
| **TX-DSP тракт** | mic gain `ZZMG`/`MG`, монитор `ZZMO`/`MO`, TX-EQ `ZZEB/ER/ET`, компрессор `ZZPK/ZZPL/CF CI CL CM`, two-tone `ZZUS`, VAC `ZZVB/ZZVF` | нет обработки передачи |
| **CW-кейер / CWX** | `ZZKM ZZKS ZZKY ZZCB ZZCD ZZCS`, `KY KS` | нет кейера |
| **RTTY / DIGI** | `ZZRA ZZRL`, ext `RF RH RM RS RY` | нет RTTY-движка |
| **RIT / XIT** | `RT XT RC RU RD` (как RIT), ext `RB RF RH` | нет смещения приёма |
| **Diversity** | `ZZDA(div) ZZDB`, ext `DA` | нет диверсити-приёма |
| **Спектр/водопад (внутр.)** | ext `SV SW SX SY SZ WT WU WV WW` | не управляется по CAT |
---
## 5. Транспорты
- **Serial** (`CATSerial.pas`): до 4 портов, параметры (порт/скорость/биты/
чётность/стоп) из `TGlobalSettings.CATSerial*`, применяются
`TCATAdapter.ApplySettings`. На текущей платформе сборки может быть
`{$DEFINE CAT_SERIAL_STUB}` (см. начало файла) — проверить перед релизом.
- **TCP** (`CATTcp.pas`): сервер на `CATTcpPort`, включается флагом
`CATTcpEnabled`. Клиенты идентифицируются GUID через `ZZGA`/`ZZGR`.
---
## 6. Как добавить новую CAT-команду (чек-лист)
1. **Сверить формат** в `CATStructs.xml` + `CATCommands.cs` (Thetis).
2. Если нужна новая возможность контроллера — есть ли публичный метод/поле
в `TRadioController`? Если нет — добавить (по образцу `SetSplit`).
3. В `CATEngine.pas`: добавить callback(и) в `TCATContext`, `SafeGet*`-хелпер
для GET, переписать тело команды (`CmdXX` или `ZZXX`).
4. В `CATAdapter.pas`: геттер (чтение `FController.F*`), сеттер + `SyncXX`
(через `Invoke`), регистрация в `TCATAdapter.Create`.
5. Если команда `ZZ*` ещё не в диспетчере — добавить ветку `else if ext = '..'`.
6. Сборка-проверка: `lazbuild -B --ws=qt6 ewsdr.lpr` (GUI-граф включает CAT;
headless-демон `ewsdrd` CAT **не** содержит).