From 4a92624e9b7dc019b7b854493f5103d4643f0513 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Fri, 26 Jun 2026 10:35:35 +0300 Subject: [PATCH] feat(cat): wire split/FM/CTUN/AGC-T CAT commands to controller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Реализованы команды, ранее бывшие заглушками — функционал уже был в 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 --- CATAdapter.pas | 121 ++++++++++++++++ CATEngine.pas | 333 +++++++++++++++++++++++++++++++++++--------- RadioController.pas | 12 ++ doc/CAT_STATUS.md | 211 ++++++++++++++++++++++++++++ 4 files changed, 614 insertions(+), 63 deletions(-) create mode 100644 doc/CAT_STATUS.md diff --git a/CATAdapter.pas b/CATAdapter.pas index 8eae4aa..fe7ac9e 100644 --- a/CATAdapter.pas +++ b/CATAdapter.pas @@ -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. diff --git a/CATEngine.pas b/CATEngine.pas index 6dd82d4..169e6d4 100644 --- a/CATEngine.pas +++ b/CATEngine.pas @@ -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=0/1 RX, lll=000–255). GET: . +// Шкала 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, 000–100 %). Уровень>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; diff --git a/RadioController.pas b/RadioController.pas index 72a6ee0..b9dee66 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -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; diff --git a/doc/CAT_STATUS.md b/doc/CAT_STATUS.md new file mode 100644 index 0000000..c243a72 --- /dev/null +++ b/doc/CAT_STATUS.md @@ -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 строк) | Движок протокола: разбор `;`, формирование ответов, реализации всех команд 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: ; (suffix = значение) + GET: ; (suffix пустой) + Resp: ; + 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 0000–0030) | `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 0–255), `ZZSQ` (0–100 %), `ZZSO` (вкл/выкл). +- Маппинг: `SetFMSquelch` / `SetFMSquelchLevel`. Уровень>0 включает + шумоподавитель. + +**Группа 3 — FM CTCSS** +- `CT`/`ZZTA` (вкл/выкл), `CN` (1-based тон 01–38), `ZZTB` (0-based 00–37). +- Маппинг: `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` (индекс 00–03: 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` (кламп 20–120 dB) | +| `ZZAU`/`ZZBP` | сдвиг VFO A/B вверх на шаг `nn` (00–14) | **исправлен баг**: были заалиашены на `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` (`` / ``) и телом +> метода в `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 **не** содержит).