Files
ewsdr/CATEngine.pas
ew8bakandClaude Opus 5 2d15da0462 feat(cat): CAT-порт на каждый слайс + Auto TX (эталон SmartSDR CAT)
Внешняя программа управляет отдельным слайсом как отдельным трансивером
через свой TCP-порт. COM-порты и 19090 остаются за главным приёмником
(слайс A) и не тронуты.

Протокол не дублируется: TCATEngine — чистый парсер поверх записи
callback'ов TCATContext, TCATTcpServer — транспорт поверх движка, поэтому
слайс-CAT = тот же движок с другим контекстом. Новый TCATSliceEndpoint
(движок + TCP-сервер на слот), массивом владеет TCATAdapter. Привязка к
СЛОТУ (буква B..G), не к Id: порт живёт, даже когда слайса нет (PS0).

Auto TX = «Auto Switch TX Slice» из SmartSDR CAT. Вся логика в одной точке
— TRadioController.RequestSliceTx, исполняется в потоке контроллера:
  • пока кто-то уже в эфире (любой источник) — заявка игнорируется,
    перехвата передачи нет никогда;
  • без Auto TX слайс передаёт, только если уже выбран TX-источником;
  • RX; снимает только СВОЮ передачу;
  • TX-источник после отпускания остаётся на слайсе (как в SmartSDR).
На флаге слайса бейдж TX → AutoTX.

Частота слайса — TuneSliceInBand: свобода в пределах ВКЛЮЧЁННОГО диапазона
(трансвертер → его FreqBegin/FreqEnd, иначе band-план), другой диапазон —
отказ. Вне захваченной полосы окно DDC переезжает: доп. пан — центром на
цель (rfPanFreq → UI перекладывает флаги/шапку/зум-бар), пан 0 — центр
посередине между целью и главным VFO и только если оба влезают (общее
железное окно, главный приёмник не оглушаем).

Настройки: вкладка Slices (enable/порт 19091../Auto TX на слайс), персист
в секции cat + device-blob.

Попутно:
  • ZZFL/ZZFH больше не заглушки — новые callback'и кромок фильтра
    (у главного nil → прежний дефолт режима), хелпер SignedPad;
  • FilterIdxFor, AGCModeToUI, BoardUtils.BandEdges;
  • DSP-состояние слайса (NR/NB/SNB/ANF) зеркалится в TCtrlSlice — флаг
    слайса рисовал нули и AGC главного;
  • FUIReady в TMainForm.OnControllerState: адаптеры, создаваемые по ходу
    FormCreate, больше не могут уронить старт рендером до постройки
    виджетов (на этом падал Auto TX из настроек).

Проверено вживую: все порты 19091..19096 слушают, PS;/ZZFA;/MD;/ID;/SM0;/
ZZTX; отвечают; старт со всеми включёнными слайсами чист под gdb.
Документ — doc/CAT_SLICES.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 13:56:36 +03:00

2362 lines
83 KiB
ObjectPascal
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit CATEngine;
{
CATEngine.pas — CAT command engine (Kenwood + Thetis ZZ* extensions).
Совместимость с Thetis/PowerSDR: все команды из CATParser.cs + CATCommands.cs
реализованы. Команды, для которых в ewsdr ещё нет функционала, возвращают
заглушку и сопровождены комментарием "STUB: <причина>".
Формат команд:
SET: <PREFIX><SUFFIX>;<CR> (suffix = значение)
GET: <PREFIX>;<CR> (suffix пустой)
Resp: <PREFIX><VALUE>;
Err: ?; (неверный формат) | E; (ошибка выполнения)
Пример:
"FA00014250000;" — set VFO A = 14.250 MHz
"FA;" — get VFO A
"ZZFA00014250000;" — то же расширенной командой
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, Math, SyncObjs, RadioModes;
const
CAT_ERROR = '?;';
CAT_EXEC_ERROR = 'E;';
CAT_ID_NUM = '019'; // TS-2000 compatible
CAT_MODE_COUNT = RadioModes.MODE_COUNT;
// 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;
TCATGetBoolFunc = function: Boolean of object;
TCATSetDoubleProc = procedure(V: Double) of object;
TCATSetIntProc = procedure(V: Integer) of object;
TCATSetBoolProc = procedure(V: Boolean) of object;
TCATProc = procedure of object;
// Andromeda front panel input
TCATPanelEncoderProc = procedure(Enc, Step: Integer) of object;
TCATPanelButtonProc = procedure(Btn: Integer; State, LongPress: Boolean) of object;
TCATContext = record
GetVfoA: TCATGetDoubleFunc;
GetVfoB: TCATGetDoubleFunc;
GetMode: TCATGetIntFunc;
GetActiveVfo: TCATGetIntFunc;
GetAGCMode: TCATGetIntFunc;
GetVolume: TCATGetIntFunc;
GetDriveLevel: TCATGetIntFunc;
GetFilterIdx: TCATGetIntFunc;
GetFilterBW: TCATGetIntFunc;
GetNRMode: TCATGetIntFunc;
GetNBMode: TCATGetIntFunc;
GetSNBEnabled: TCATGetBoolFunc;
GetANFEnabled: TCATGetBoolFunc;
GetTransmitting: TCATGetBoolFunc;
GetRunning: TCATGetBoolFunc;
GetSMeter: TCATGetDoubleFunc;
GetCurrentBand: TCATGetIntFunc;
SetVfoA: TCATSetDoubleProc;
SetVfoB: TCATSetDoubleProc;
SetMode: TCATSetIntProc;
SetActiveVfo: TCATSetIntProc;
SetAGCMode: TCATSetIntProc;
SetVolume: TCATSetIntProc;
SetDriveLevel: TCATSetIntProc;
SetFilterIdx: TCATSetIntProc;
SetNRMode: TCATSetIntProc;
SetNBMode: TCATSetIntProc;
SetSNBEnabled: TCATSetBoolProc;
SetANFEnabled: TCATSetBoolProc;
SetTransmitting: TCATSetBoolProc;
DoBandUp: TCATProc;
DoBandDown: TCATProc;
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;
// Кромки фильтра в Гц (ZZFL/ZZFH). Главный приёмник настраивается пресетами
// (callback'и nil → команды отвечают дефолтом режима), слайс — кромками.
GetFilterLow: TCATGetIntFunc;
GetFilterHigh: TCATGetIntFunc;
SetFilterLow: TCATSetIntProc;
SetFilterHigh: TCATSetIntProc;
// Andromeda front panel input (ZZZD/ZZZU/ZZZE/ZZZP/ZZZS)
OnPanelVfoStep: TCATSetIntProc; // знаковое число шагов энкодера VFO
OnPanelEncoder: TCATPanelEncoderProc; // номер энкодера (0-based) + знаковый шаг
OnPanelButton: TCATPanelButtonProc; // номер кнопки (0-based) + state/long-press
OnPanelVersion: TCATSetIntProc; // версия HW/SW панели (рукопожатие)
end;
TCATEngine = class
private
FCtx: TCATContext;
FLock: TCriticalSection;
function Pad(V: Int64; W: Integer): string;
function SignedPad(V: Int64; W: Integer): string; // '+nnn' / '-nnn'
function FreqToStr(Hz: Double): string;
function StrToFreq(const S: string): Double;
function ModeToK(M: Integer): Integer;
function KToMode(K: Integer): Integer;
function SMeterRaw(dBm: Double): Integer;
// helpers to safely call context callbacks
function SafeGetVfoA: Double;
function SafeGetVfoB: Double;
function SafeGetMode: Integer;
function SafeGetVolume: Integer;
function SafeGetDriveLevel: Integer;
function SafeGetFilterIdx: Integer;
function SafeGetAGCMode: Integer;
function SafeGetNRMode: Integer;
function SafeGetNBMode: Integer;
function SafeGetSNB: Boolean;
function SafeGetANF: Boolean;
function SafeGetTX: Boolean;
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;
function CmdAI(const s: string): string;
function CmdBD: string;
function CmdBU: string;
function CmdCN(const s: string): string;
function CmdCT(const s: string): string;
function CmdDN: string;
function CmdFA(const s: string): string;
function CmdFB(const s: string): string;
function CmdFR(const s: string): string;
function CmdFT(const s: string): string;
function CmdFW(const s: string): string;
function CmdGT(const s: string): string;
function CmdID: string;
function CmdIF: string;
function CmdKS(const s: string): string;
function CmdKY(const s: string): string;
function CmdMD(const s: string): string;
function CmdMG(const s: string): string;
function CmdMO(const s: string): string;
function CmdNB(const s: string): string;
function CmdNT(const s: string): string;
function CmdOF(const s: string): string;
function CmdOS(const s: string): string;
function CmdPC(const s: string): string;
function CmdPR(const s: string): string;
function CmdPS(const s: string): string;
function CmdQI: string;
function CmdRC: string;
function CmdRD(const s: string): string;
function CmdRT(const s: string): string;
function CmdRU(const s: string): string;
function CmdRX(const s: string): string;
function CmdSH(const s: string): string;
function CmdSL(const s: string): string;
function CmdSM(const s: string): string;
function CmdSQ(const s: string): string;
function CmdTX(const s: string): string;
function CmdUP: string;
function CmdXT(const s: string): string;
// ZZ extended
function ParseZZ(const ext, s: string): string;
function ZZAA(const s: string): string;
function ZZAB(const s: string): string;
function ZZAC(const s: string): string;
function ZZAG(const s: string): string;
function ZZAI(const s: string): string;
function ZZAP(const s: string): string;
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;
function ZZBS(const s: string): string;
function ZZBU: string;
function ZZCB(const s: string): string;
function ZZCD(const s: string): string;
function ZZCN(const s: string): string;
function ZZCS(const s: string): string;
function ZZDA(const s: string): string;
function ZZDB(const s: string): string;
function ZZDM(const s: string): string;
function ZZDN(const s: string): string;
function ZZDU: string;
function ZZEA(const s: string): string;
function ZZEB(const s: string): string;
function ZZEM(const s: string): string;
function ZZFA(const s: string): string;
function ZZFB(const s: string): string;
function ZZFD(const s: string): string;
function ZZFI(const s: string): string;
function ZZFL(const s: string): string;
function ZZFH(const s: string): string;
function ZZFM: string;
function ZZFT(const s: string): string;
function ZZGA(const s: string): string;
function ZZGR(const s: string): string;
function ZZGT(const s: string): string;
function ZZID: string;
function ZZIF(const s: string): string;
function ZZIO: string;
function ZZIS(const s: string): string;
function ZZKM(const s: string): string;
function ZZKS(const s: string): string;
function ZZKY(const s: string): string;
function ZZMA(const s: string): string;
function ZZMB(const s: string): string;
function ZZMD(const s: string): string;
function ZZMG(const s: string): string;
function ZZML: string;
function ZZMN(const s: string): string;
function ZZMO(const s: string): string;
function ZZMV: string;
function ZZMY: string;
function ZZNA(const s: string): string;
function ZZNB(const s: string): string;
function ZZNL(const s: string): string;
function ZZNN(const s: string): string;
function ZZNR(const s: string): string;
function ZZNS(const s: string): string;
function ZZNT(const s: string): string;
function ZZOA(const s: string): string;
function ZZOB(const s: string): string;
function ZZPC(const s: string): string;
function ZZPD: string;
function ZZPO(const s: string): string;
function ZZPS(const s: string): string;
function ZZQM: string;
function ZZQR: string;
function ZZQS: string;
function ZZRA(const s: string): string;
function ZZRC: string;
function ZZRD(const s: string): string;
function ZZRL(const s: string): string;
function ZZRU(const s: string): string;
function ZZRV: string;
function ZZRX(const s: string): string;
function ZZSA: string;
function ZZSB: string;
function ZZSM(const s: string): string;
function ZZSO(const s: string): string;
function ZZSP(const s: string): string;
function ZZSQ(const s: string): string;
function ZZSR(const s: string): string;
function ZZST(const s: string): string;
function ZZSU: string;
function ZZTA(const s: string): string;
function ZZTB(const s: string): string;
function ZZTX(const s: string): string;
function ZZUP(const s: string): string;
function ZZUS: string;
function ZZUT(const s: string): string;
function ZZUX(const s: string): string;
function ZZUY(const s: string): string;
function ZZVA(const s: string): string;
function ZZVB(const s: string): string;
function ZZVG(const s: string): string;
function ZZVN: string;
function ZZVS(const s: string): string;
function ZZWA(const s: string): string;
function ZZWB(const s: string): string;
function ZZWC(const s: string): string;
function ZZWD(const s: string): string;
function ZZWE(const s: string): string;
function ZZWF(const s: string): string;
function ZZWG(const s: string): string;
function ZZWH(const s: string): string;
function ZZWJ(const s: string): string;
function ZZWK(const s: string): string;
function ZZWL(const s: string): string;
function ZZWM(const s: string): string;
function ZZWN(const s: string): string;
function ZZWO(const s: string): string;
function ZZWP(const s: string): string;
function ZZWQ(const s: string): string;
function ZZWR(const s: string): string;
function ZZWS(const s: string): string;
function ZZXC: string;
function ZZXF(const s: string): string;
function ZZXA(const s: string): string;
function ZZYA(const s: string): string;
function ZZYB(const s: string): string;
function ZZYC(const s: string): string;
function ZZYR(const s: string): string;
function ZZZA(const s: string): string;
function ZZZB: string;
function ZZZD(const s: string): string;
function ZZZE(const s: string): string;
function ZZZM(const s: string): string;
function ZZZP(const s: string): string;
function ZZZQ(const s: string): string;
function ZZZR(const s: string): string;
function ZZZS(const s: string): string;
function ZZZT(const s: string): string;
function ZZZU(const s: string): string;
function ZZZV(const s: string): string;
function ZZZW(const s: string): string;
function ZZZZ: string;
function ZZZO(const s: string): string;
public
constructor Create(const Ctx: TCATContext);
destructor Destroy; override;
function Parse(const Cmd: string): string;
end;
implementation
{ ── Local helpers ────────────────────────────────────────────────────────── }
// Convert Boolean to CAT '1'/'0' string — avoids IfThen char-overload ambiguity
function B2C(B: Boolean): string; inline;
begin if B then Result := '1' else Result := '0'; end;
{ ── Helpers ──────────────────────────────────────────────────────────────── }
function TCATEngine.Pad(V: Int64; W: Integer): string;
begin
Result := IntToStr(Abs(V));
while Length(Result) < W do Result := '0' + Result;
if Length(Result) > W then Result := Copy(Result, Length(Result) - W + 1, W);
end;
function TCATEngine.SignedPad(V: Int64; W: Integer): string;
// Знаковое поле фиксированной ширины (ZZAR/ZZFL/ZZFH): знак + W цифр.
begin
if V >= 0 then Result := '+' + Pad(V, W)
else Result := '-' + Pad(-V, W);
end;
function TCATEngine.FreqToStr(Hz: Double): string;
begin
Result := Pad(Round(Hz), 11);
end;
function TCATEngine.StrToFreq(const S: string): Double;
var V: Int64;
begin
if TryStrToInt64(S, V) then
Result := V
else
Result := 0;
end;
function TCATEngine.ModeToK(M: Integer): Integer;
begin
case M of
MODE_LSB: Result := 1;
MODE_USB: Result := 2;
MODE_DSB: Result := 2; // treated as USB for CAT compatibility
MODE_CWU: Result := 3;
MODE_FM: Result := 4;
MODE_WFM: Result := 4; // treated as FM
MODE_DMR: Result := 4; // DMR RF transport is narrow FM
MODE_FMRAW: Result := 4; // raw discriminator is still FM transport
MODE_AM: Result := 5;
MODE_SAM: Result := 5; // treated as AM
MODE_DIGU: Result := 6; // Kenwood FSK
MODE_CWL: Result := 7;
MODE_DIGL: Result := 9; // Kenwood FSK-R
else Result := 2;
end;
end;
function TCATEngine.KToMode(K: Integer): Integer;
begin
case K of
1: Result := MODE_LSB;
2: Result := MODE_USB;
3: Result := MODE_CWU;
4: Result := MODE_FM;
5: Result := MODE_AM;
6: Result := MODE_DIGU; // FSK/RTTY
7: Result := MODE_CWL;
9: Result := MODE_DIGL; // FSK-R
else Result := MODE_USB;
end;
end;
function TCATEngine.SMeterRaw(dBm: Double): Integer;
var v: Double;
begin
v := (dBm + 140.0) * 2.0;
if v < 0 then v := 0;
if v > 260 then v := 260;
Result := Round(v);
end;
{ ── Safe getters (handle nil callbacks gracefully) ───────────────────────── }
function TCATEngine.SafeGetVfoA: Double;
begin
if Assigned(FCtx.GetVfoA) then Result := FCtx.GetVfoA else Result := 14200000;
end;
function TCATEngine.SafeGetVfoB: Double;
begin
if Assigned(FCtx.GetVfoB) then Result := FCtx.GetVfoB else Result := 14200000;
end;
function TCATEngine.SafeGetMode: Integer;
begin
if Assigned(FCtx.GetMode) then Result := FCtx.GetMode else Result := MODE_USB;
end;
function TCATEngine.SafeGetVolume: Integer;
begin
if Assigned(FCtx.GetVolume) then Result := FCtx.GetVolume else Result := 50;
end;
function TCATEngine.SafeGetDriveLevel: Integer;
begin
if Assigned(FCtx.GetDriveLevel) then Result := FCtx.GetDriveLevel else Result := 50;
end;
function TCATEngine.SafeGetFilterIdx: Integer;
begin
if Assigned(FCtx.GetFilterIdx) then Result := FCtx.GetFilterIdx else Result := 0;
end;
function TCATEngine.SafeGetAGCMode: Integer;
begin
if Assigned(FCtx.GetAGCMode) then Result := FCtx.GetAGCMode else Result := 0;
end;
function TCATEngine.SafeGetNRMode: Integer;
begin
if Assigned(FCtx.GetNRMode) then Result := FCtx.GetNRMode else Result := 0;
end;
function TCATEngine.SafeGetNBMode: Integer;
begin
if Assigned(FCtx.GetNBMode) then Result := FCtx.GetNBMode else Result := 0;
end;
function TCATEngine.SafeGetSNB: Boolean;
begin
if Assigned(FCtx.GetSNBEnabled) then Result := FCtx.GetSNBEnabled else Result := False;
end;
function TCATEngine.SafeGetANF: Boolean;
begin
if Assigned(FCtx.GetANFEnabled) then Result := FCtx.GetANFEnabled else Result := False;
end;
function TCATEngine.SafeGetTX: Boolean;
begin
if Assigned(FCtx.GetTransmitting) then Result := FCtx.GetTransmitting else Result := False;
end;
function TCATEngine.SafeGetRunning: Boolean;
begin
if Assigned(FCtx.GetRunning) then Result := FCtx.GetRunning else Result := False;
end;
function TCATEngine.SafeGetSMeter: Double;
begin
if Assigned(FCtx.GetSMeter) then Result := FCtx.GetSMeter else Result := -120;
end;
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 ─────────────────────────────────────────────── }
constructor TCATEngine.Create(const Ctx: TCATContext);
begin
inherited Create;
FCtx := Ctx;
FLock := TCriticalSection.Create;
end;
destructor TCATEngine.Destroy;
begin
FLock.Free;
inherited;
end;
{ ══════════════════════════════════════════════════════════════════════════
Parse — main entry point. Thread-safe, can be called from serial/TCP threads.
══════════════════════════════════════════════════════════════════════════ }
function TCATEngine.Parse(const Cmd: string): string;
var
s, pfx, ext, sfx: string;
tpos: Integer;
begin
Result := '';
s := Trim(Cmd);
// strip leading semicolons (some loggers prepend them)
while (Length(s) > 0) and (s[1] = ';') do Delete(s, 1, 1);
if Length(s) < 3 then Exit(CAT_ERROR);
tpos := Pos(';', s);
if tpos < 3 then Exit(CAT_ERROR);
pfx := UpperCase(Copy(s, 1, 2));
FLock.Acquire;
try
if pfx = 'ZZ' then
begin
if tpos < 5 then Exit(CAT_ERROR);
ext := UpperCase(Copy(s, 3, 2));
sfx := Copy(s, 5, tpos - 5);
Result := ParseZZ(ext, sfx);
end else begin
sfx := Copy(s, 3, tpos - 3);
if pfx = 'AG' then Result := CmdAG(sfx)
else if pfx = 'AI' then Result := CmdAI(sfx)
else if pfx = 'BD' then begin CmdBD; Result := ''; end
else if pfx = 'BU' then begin CmdBU; Result := ''; end
else if pfx = 'CN' then Result := CmdCN(sfx)
else if pfx = 'CT' then Result := CmdCT(sfx)
else if pfx = 'DN' then begin CmdDN; Result := ''; end
else if pfx = 'FA' then Result := CmdFA(sfx)
else if pfx = 'FB' then Result := CmdFB(sfx)
else if pfx = 'FR' then Result := CmdFR(sfx)
else if pfx = 'FT' then Result := CmdFT(sfx)
else if pfx = 'FW' then Result := CmdFW(sfx)
else if pfx = 'GT' then Result := CmdGT(sfx)
else if pfx = 'ID' then Result := 'ID' + CmdID + ';'
else if pfx = 'IF' then Result := 'IF' + CmdIF + ';'
else if pfx = 'KS' then Result := CmdKS(sfx)
else if pfx = 'KY' then Result := CmdKY(sfx)
else if pfx = 'MD' then Result := CmdMD(sfx)
else if pfx = 'MG' then Result := CmdMG(sfx)
else if pfx = 'MO' then Result := CmdMO(sfx)
else if pfx = 'NB' then Result := CmdNB(sfx)
else if pfx = 'NT' then Result := CmdNT(sfx)
else if pfx = 'OF' then Result := CmdOF(sfx)
else if pfx = 'OS' then Result := CmdOS(sfx)
else if pfx = 'PC' then Result := CmdPC(sfx)
else if pfx = 'PR' then Result := CmdPR(sfx)
else if pfx = 'PS' then Result := CmdPS(sfx)
else if pfx = 'QI' then begin CmdQI; Result := ''; end
else if pfx = 'RC' then begin CmdRC; Result := ''; end
else if pfx = 'RD' then begin CmdRD(sfx); Result := ''; end
else if pfx = 'RT' then Result := CmdRT(sfx)
else if pfx = 'RU' then begin CmdRU(sfx); Result := ''; end
else if pfx = 'RX' then begin CmdRX(sfx); Result := ''; end
else if pfx = 'SH' then Result := CmdSH(sfx)
else if pfx = 'SL' then Result := CmdSL(sfx)
else if pfx = 'SM' then Result := CmdSM(sfx)
else if pfx = 'SQ' then Result := CmdSQ(sfx)
else if pfx = 'TX' then begin CmdTX(sfx); Result := ''; end
else if pfx = 'UP' then begin CmdUP; Result := ''; end
else if pfx = 'XT' then Result := CmdXT(sfx)
else Result := CAT_ERROR;
// Wrap standard command response
if (Result <> '') and (Result <> CAT_ERROR) and (Result <> CAT_EXEC_ERROR) then
if Copy(Result, 1, 2) <> pfx then
Result := pfx + Result + ';';
end;
finally
FLock.Release;
end;
end;
{ ══════════════════════════════════════════════════════════════════════════
Standard commands (non-ZZ)
══════════════════════════════════════════════════════════════════════════ }
function TCATEngine.CmdAG(const s: string): string;
// AG: SET=4 chars (0xxx), GET=1 char ('0'=RX1, '1'=RX2)
// Scale 0255 (Kenwood) ↔ 0100 (ewsdr)
var v: Integer;
begin
if Length(s) = 4 then begin
v := Round(StrToIntDef(Copy(s,2,3), 0) / 2.55);
v := EnsureRange(v, 0, 100);
if Assigned(FCtx.SetVolume) then FCtx.SetVolume(v);
Result := '';
end else if Length(s) = 1 then begin
v := Round(SafeGetVolume / 0.392);
Result := '0' + Pad(v, 3);
end else
Result := CAT_ERROR;
end;
function TCATEngine.CmdAI(const s: string): string;
// AI: auto-information — maps to ZZAI
begin
Result := ZZAI(s);
end;
function TCATEngine.CmdBD: string;
begin
if Assigned(FCtx.DoBandDown) then FCtx.DoBandDown;
Result := '';
end;
function TCATEngine.CmdBU: string;
begin
if Assigned(FCtx.DoBandUp) then FCtx.DoBandUp;
Result := '';
end;
function TCATEngine.CmdCN(const s: string): string;
// CN: CTCSS tone number (2 digits, 1-based 01..38 по таблице CTCSS_TONES).
var idx: Integer;
begin
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 (1=on, 0=off) для FM-передачи.
begin
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.CmdDN: string;
begin
if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown;
Result := '';
end;
function TCATEngine.CmdFA(const s: string): string;
// FA: VFO A frequency (11 digits, Hz)
begin
if Length(s) = 11 then begin
if Assigned(FCtx.SetVfoA) then FCtx.SetVfoA(StrToFreq(s));
Result := '';
end else if Length(s) = 0 then
Result := FreqToStr(SafeGetVfoA)
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdFB(const s: string): string;
// FB: VFO B frequency (11 digits, Hz)
begin
if Length(s) = 11 then begin
if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(StrToFreq(s));
Result := '';
end else if Length(s) = 0 then
Result := FreqToStr(SafeGetVfoB)
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdFR(const s: string): string;
// FR: receive VFO (0=VFO A, 1=VFO B) — выбирает активный приёмный VFO.
begin
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;
function TCATEngine.CmdFT(const s: string): string;
// FT: transmit VFO / split — maps to ZZSP
begin
Result := ZZSP(s);
end;
function TCATEngine.CmdFW(const s: string): string;
// FW: filter width (4 digits) — maps to filter index
// Thetis encodes filter by index into 4-digit string 0000-0009
var idx: Integer;
begin
if Length(s) = 4 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 9);
if Assigned(FCtx.SetFilterIdx) then FCtx.SetFilterIdx(idx);
Result := '';
end else if Length(s) = 0 then
Result := Pad(SafeGetFilterIdx, 4)
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdGT(const s: string): string;
// GT: AGC mode (3 digits) — 0=FAST,1=MED,2=SLOW,3=LONG,4=OFF
begin
if Length(s) = 3 then begin
if Assigned(FCtx.SetAGCMode) then FCtx.SetAGCMode(EnsureRange(StrToIntDef(s,0),0,4));
Result := '';
end else if Length(s) = 0 then
Result := Pad(SafeGetAGCMode, 3)
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdID: string;
begin
Result := CAT_ID_NUM;
end;
function TCATEngine.CmdIF: string;
// IF: interface status — 37-byte composite response
var
freq, step, incr, rit, xit, tx, mode, split: string;
m: Integer;
begin
freq := FreqToStr(SafeGetVfoA);
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 := B2C(SafeGetSplit);
Result := freq + step + incr + rit + xit + '000' + tx + mode + '0' + '0' + split + '0000';
end;
function TCATEngine.CmdKS(const s: string): string;
// KS: CW keying speed (3 digits, WPM) — STUB: no CW keyer
begin
if Length(s) = 3 then
Result := '' // accepted but ignored
else if Length(s) = 0 then
Result := '025' // default 25 WPM
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdKY(const s: string): string;
// KY: send CW text — STUB: no CW keyer implemented
begin
Result := '0'; // queue not full
end;
function TCATEngine.CmdMD(const s: string): string;
// MD: mode (1 char Kenwood) — 1=LSB,2=USB,3=CW,4=FM,5=AM,7=CW-R
var k: Integer;
begin
if Length(s) = 1 then begin
k := StrToIntDef(s, 0);
if (k >= 1) and Assigned(FCtx.SetMode) then
FCtx.SetMode(KToMode(k));
Result := '';
end else if Length(s) = 0 then
Result := IntToStr(ModeToK(SafeGetMode))
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdMG(const s: string): string;
// MG: mic gain (3 digits, 0-100) — STUB: TX DSP not yet implemented
begin
if Length(s) = 3 then
Result := ''
else if Length(s) = 0 then
Result := '050'
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdMO(const s: string): string;
// MO: monitor — STUB
begin
if Length(s) = 1 then Result := ''
else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.CmdNB(const s: string): string;
// NB: noise blanker on/off → maps to NBMode
begin
if Length(s) = 1 then begin
if Assigned(FCtx.SetNBMode) then
FCtx.SetNBMode(StrToIntDef(s, 0));
Result := '';
end else if Length(s) = 0 then
Result := IntToStr(EnsureRange(SafeGetNBMode, 0, 1))
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdNT(const s: string): string;
// NT: notch (ANF) on/off
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.CmdOF(const s: string): string;
// OF: FM repeater offset frequency (9 digits, Hz).
begin
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 (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 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;
// PC: TX power 0-100 % (3 digits) → DriveLevel
var v: Integer;
begin
if Length(s) = 3 then begin
v := EnsureRange(StrToIntDef(s,0), 0, 100);
if Assigned(FCtx.SetDriveLevel) then FCtx.SetDriveLevel(v);
Result := '';
end else if Length(s) = 0 then
Result := Pad(SafeGetDriveLevel, 3)
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdPR(const s: string): string;
// PR: speech compressor — STUB: always off
begin
if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.CmdPS(const s: string): string;
// PS: power status
begin
if Length(s) = 1 then Result := '' // accept but ignore
else if Length(s) = 0 then
Result := B2C(SafeGetRunning)
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdQI: string;
// QI: store VFO A to quick memory — STUB
begin
Result := '';
end;
function TCATEngine.CmdRC: string;
// RC: clear RIT — STUB: no RIT
begin
Result := '';
end;
function TCATEngine.CmdRD(const s: string): string;
// RD: decrease RIT — maps to tune down
begin
if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown;
Result := '';
end;
function TCATEngine.CmdRT(const s: string): string;
// RT: RIT on/off — STUB: no RIT
begin
if Length(s) = 1 then Result := ''
else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.CmdRU(const s: string): string;
// RU: increase RIT — maps to tune up
begin
if Assigned(FCtx.DoTuneUp) then FCtx.DoTuneUp;
Result := '';
end;
function TCATEngine.CmdRX(const s: string): string;
// RX: switch to receive
begin
if Assigned(FCtx.SetTransmitting) then FCtx.SetTransmitting(False);
Result := '';
end;
function TCATEngine.CmdSH(const s: string): string;
// SH: high pass filter setting (2 digits, 0-based index) — maps to filter index
var idx: Integer;
begin
if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 9);
if Assigned(FCtx.SetFilterIdx) then FCtx.SetFilterIdx(idx);
Result := '';
end else if Length(s) = 0 then
Result := Pad(SafeGetFilterIdx, 2)
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdSL(const s: string): string;
// SL: low pass filter setting — mirrors SH
begin
Result := CmdSH(s);
end;
function TCATEngine.CmdSM(const s: string): string;
// SM: S-meter read (1 char VFO selector, response 4 chars 0000-0015)
// Kenwood SM answer: 00000015 → S0S9+60dB
// We return 4-digit raw 0000-0030 compatible format
var sm: Integer;
begin
if (Length(s) = 1) and ((s = '0') or (s = '1')) then begin
sm := SMeterRaw(SafeGetSMeter);
// Scale 0-260 → 0-30 (Kenwood SM scale 0000-0030)
sm := Round(sm * 30 / 260);
Result := Pad(sm, 4);
end else
Result := CAT_ERROR;
end;
function TCATEngine.CmdSQ(const s: string): string;
// 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 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;
// TX: switch to transmit
begin
if Assigned(FCtx.SetTransmitting) then FCtx.SetTransmitting(True);
Result := '';
end;
function TCATEngine.CmdUP: string;
begin
if Assigned(FCtx.DoTuneUp) then FCtx.DoTuneUp;
Result := '';
end;
function TCATEngine.CmdXT(const s: string): string;
// XT: XIT on/off — STUB: no XIT
begin
if Length(s) = 1 then Result := ''
else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
{ ══════════════════════════════════════════════════════════════════════════
ZZ* extended commands dispatcher
══════════════════════════════════════════════════════════════════════════ }
function TCATEngine.ParseZZ(const ext, s: string): string;
var ans: string;
begin
ans := '';
if ext = 'AA' then ans := ZZAA(s)
else if ext = 'AB' then ans := ZZAB(s)
else if ext = 'AC' then ans := ZZAC(s)
else if ext = 'AD' then ans := '' // STUB: TX freq display
else if ext = 'AE' then ans := '' // STUB: RIT value
else if ext = 'AF' then ans := '' // STUB: XIT value
else if ext = 'AG' then ans := ZZAG(s)
else if ext = 'AI' then ans := ZZAI(s)
else if ext = 'AP' then ans := ZZAP(s)
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 := 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
else if ext = 'BD' then begin ZZBD; ans := ''; end
else if ext = 'BE' then ans := ZZBE(s)
else if ext = 'BF' then ans := '' // STUB: sub-RX filter
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 := 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
else if ext = 'BU' then begin ZZBU; ans := ''; end
else if ext = 'BY' then ans := '' // STUB: VFO bypass
else if ext = 'CB' then ans := ZZCB(s)
else if ext = 'CD' then ans := ZZCD(s)
else if ext = 'CF' then ans := '' // STUB: compressor freq
else if ext = 'CI' then ans := '' // STUB: compressor in
else if ext = 'CL' then ans := '' // STUB: compressor level
else if ext = 'CM' then ans := '' // STUB: compressor mode
else if ext = 'CN' then ans := ZZCN(s)
else if ext = 'CO' then ans := '' // STUB: compressor out
else if ext = 'CP' then ans := '' // STUB: compressor peak
else if ext = 'CS' then ans := ZZCS(s)
else if ext = 'CT' then ans := '' // STUB: compressor threshold
else if ext = 'CU' then begin ZZDU; ans := ''; end
else if ext = 'DA' then ans := ZZDA(s)
else if ext = 'DB' then ans := ZZDB(s)
else if ext = 'DC' then ans := '' // STUB: display contrast
else if ext = 'DD' then ans := '' // STUB: DSP disable
else if ext = 'DE' then ans := '' // STUB: DSP enable
else if ext = 'DF' then ans := '' // STUB: TX filter low
else if ext = 'DG' then ans := '' // STUB: TX filter high
else if ext = 'DH' then ans := '' // STUB: TX DSP high cut
else if ext = 'DM' then ans := ZZDM(s)
else if ext = 'DN' then ans := ZZDN(s)
else if ext = 'DO' then ans := '' // STUB: sub-RX on
else if ext = 'DP' then ans := '' // STUB: display position
else if ext = 'DQ' then ans := '' // STUB: sub-RX
else if ext = 'DR' then ans := '' // STUB: sub-RX
else if ext = 'DU' then begin ZZDU; ans := ''; end
else if ext = 'DX' then ans := '' // STUB: sub NB
else if ext = 'EA' then ans := ZZEA(s)
else if ext = 'EB' then ans := ZZEB(s)
else if ext = 'EM' then ans := ZZEM(s)
else if ext = 'ER' then ans := '' // STUB: EQ reset
else if ext = 'ET' then ans := '' // STUB: EQ TX
else if ext = 'FA' then ans := ZZFA(s)
else if ext = 'FB' then ans := ZZFB(s)
else if ext = 'FD' then ans := ZZFD(s)
else if ext = 'FH' then ans := ZZFH(s)
else if ext = 'FI' then ans := ZZFI(s)
else if ext = 'FJ' then ans := '' // STUB: RX2 filter
else if ext = 'FL' then ans := ZZFL(s)
else if ext = 'FM' then ans := ZZFM
else if ext = 'FR' then ans := '' // STUB: filter RX
else if ext = 'FS' then ans := '' // STUB: sub filter
else if ext = 'FT' then ans := ZZFT(s)
else if ext = 'FV' then ans := '' // STUB: filter var
else if ext = 'FW' then ans := '' // STUB: filter write
else if ext = 'FX' then ans := '' // STUB: filter extended
else if ext = 'FY' then ans := '' // STUB: filter Y
else if ext = 'GA' then ans := ZZGA(s)
else if ext = 'GE' then ans := '' // STUB: gain enable
else if ext = 'GL' then ans := '' // STUB: gain level
else if ext = 'GR' then ans := ZZGR(s)
else if ext = 'GT' then ans := ZZGT(s)
else if ext = 'GU' then ans := '' // STUB: gain update
else if ext = 'HA' then ans := '' // STUB: hardware A
else if ext = 'HR' then ans := '' // STUB: hardware read
else if ext = 'HT' then ans := '' // STUB: hardware TX
else if ext = 'HU' then ans := '' // STUB: hardware update
else if ext = 'HV' then ans := '' // STUB: hardware value
else if ext = 'HW' then ans := '' // STUB: hardware write
else if ext = 'HX' then ans := '' // STUB: hardware extended
else if ext = 'ID' then ans := ZZID
else if ext = 'IF' then ans := ZZIF(s)
else if ext = 'IO' then ans := ZZIO
else if ext = 'IS' then ans := ZZIS(s)
else if ext = 'IT' then ans := '' // STUB: initial TX
else if ext = 'IU' then ans := '' // STUB: initial update
else if ext = 'JO' then ans := '' // STUB: JSON
else if ext = 'JP' then ans := '' // STUB: JSON preset
else if ext = 'JQ' then ans := '' // STUB: JSON query
else if ext = 'JR' then ans := '' // STUB: JSON read
else if ext = 'JS' then ans := '' // STUB: JSON status
else if ext = 'KM' then ans := ZZKM(s)
else if ext = 'KO' then ans := '' // STUB: key on
else if ext = 'KS' then ans := ZZKS(s)
else if ext = 'KY' then ans := ZZKY(s)
else if ext = 'LA' then ans := '' // STUB: level A
else if ext = 'LB' then ans := '' // STUB: level B
else if ext = 'LC' then ans := '' // STUB: level C
else if ext = 'LD' then ans := '' // STUB: level D
else if ext = 'LE' then ans := '' // STUB: level E
else if ext = 'LF' then ans := '' // STUB: level F
else if ext = 'LG' then ans := '' // STUB: level G
else if ext = 'LH' then ans := '' // STUB: level H
else if ext = 'LI' then ans := '' // STUB: level I
else if ext = 'MA' then ans := ZZMA(s)
else if ext = 'MB' then ans := ZZMB(s)
else if ext = 'MD' then ans := ZZMD(s)
else if ext = 'ME' then ans := '' // STUB: mode enable
else if ext = 'MF' then ans := '' // STUB: mode freq
else if ext = 'MG' then ans := ZZMG(s)
else if ext = 'ML' then ans := ZZML
else if ext = 'MN' then ans := ZZMN(s)
else if ext = 'MO' then ans := ZZMO(s)
else if ext = 'MR' then ans := '' // STUB: mode RX
else if ext = 'MS' then ans := '' // STUB: mode status
else if ext = 'MT' then ans := '' // STUB: mode TX
else if ext = 'MU' then ans := '' // STUB: mode update
else if ext = 'MV' then ans := ZZMV
else if ext = 'MW' then ans := '' // STUB: mode write
else if ext = 'MX' then ans := '' // STUB: mode extended
else if ext = 'MY' then ans := ZZMY
else if ext = 'MZ' then ans := '' // STUB: mode Z
else if ext = 'NA' then ans := ZZNA(s)
else if ext = 'NB' then ans := ZZNB(s)
else if ext = 'NC' then ans := '' // STUB: NR C
else if ext = 'ND' then ans := '' // STUB: NR D
else if ext = 'NE' then ans := '' // STUB: NR E
else if ext = 'NF' then ans := '' // STUB: NR F
else if ext = 'NG' then ans := '' // STUB: noise gate
else if ext = 'NH' then ans := '' // STUB: NR H
else if ext = 'NL' then ans := ZZNL(s)
else if ext = 'NM' then ans := '' // STUB: NR mode
else if ext = 'NN' then ans := ZZNN(s)
else if ext = 'NO' then ans := '' // STUB: sub NR
else if ext = 'NR' then ans := ZZNR(s)
else if ext = 'NS' then ans := ZZNS(s)
else if ext = 'NT' then ans := ZZNT(s)
else if ext = 'NU' then ans := '' // STUB: NR update
else if ext = 'NV' then ans := '' // STUB: NR value
else if ext = 'NW' then ans := '' // STUB: NR write
else if ext = 'OA' then ans := ZZOA(s)
else if ext = 'OB' then ans := ZZOB(s)
else if ext = 'OC' then ans := '' // STUB: output compression
else if ext = 'OD' then ans := '' // STUB: output DSP
else if ext = 'OE' then ans := '' // STUB: output E
else if ext = 'OF' then ans := '' // STUB: sub-RX freq
else if ext = 'OG' then ans := '' // STUB: output gain
else if ext = 'OH' then ans := '' // STUB: output high
else if ext = 'OJ' then ans := '' // STUB: output J
else if ext = 'OL' then ans := '' // STUB: output level
else if ext = 'OS' then ans := '' // STUB: output status
else if ext = 'OT' then ans := '' // STUB: FM repeater offset
else if ext = 'OU' then ans := '' // STUB: output update
else if ext = 'OV' then ans := '' // STUB: output value
else if ext = 'OW' then ans := '' // STUB: output write
else if ext = 'OX' then ans := '' // STUB: oxide extra
else if ext = 'OZ' then ans := '' // STUB: oxide Z
else if ext = 'PA' then ans := '' // STUB: sub-RX pan
else if ext = 'PB' then ans := '' // STUB: power B
else if ext = 'PC' then ans := ZZPC(s)
else if ext = 'PD' then ans := ZZPD
else if ext = 'PE' then ans := '' // STUB: power enable
else if ext = 'PO' then ans := ZZPO(s)
else if ext = 'PS' then ans := ZZPS(s)
else if ext = 'PY' then ans := '' // STUB: power Y
else if ext = 'PZ' then ans := '' // STUB: power Z
else if ext = 'QK' then ans := '' // STUB: query keep
else if ext = 'QM' then ans := ZZQM
else if ext = 'QR' then ans := ZZQR
else if ext = 'QS' then ans := ZZQS
else if ext = 'RA' then ans := ZZRA(s)
else if ext = 'RB' then ans := '' // STUB: RIT B
else if ext = 'RC' then begin ZZRC; ans := ''; end
else if ext = 'RD' then begin ZZRD(s); ans := ''; end
else if ext = 'RF' then ans := '' // STUB: RIT freq
else if ext = 'RH' then ans := '' // STUB: RIT high
else if ext = 'RL' then ans := ZZRL(s)
else if ext = 'RM' then ans := '' // STUB: RIT mode
else if ext = 'RS' then ans := '' // STUB: RIT status
else if ext = 'RT' then ans := '' // STUB: RIT transmit
else if ext = 'RU' then begin ZZRU(s); ans := ''; end
else if ext = 'RV' then ans := ZZRV
else if ext = 'RX' then ans := ZZRX(s)
else if ext = 'RY' then ans := '' // STUB: RIT Y
else if ext = 'SA' then begin ZZSA; ans := ''; end
else if ext = 'SB' then begin ZZSB; ans := ''; end
else if ext = 'SD' then ans := '' // STUB: sub-RX display
else if ext = 'SF' then ans := '' // STUB: sweep freq
else if ext = 'SG' then ans := '' // STUB: sub gain
else if ext = 'SH' then ans := '' // STUB: sub high cut
else if ext = 'SM' then ans := ZZSM(s)
else if ext = 'SN' then ans := '' // STUB: serial number
else if ext = 'SO' then ans := ZZSO(s)
else if ext = 'SP' then ans := ZZSP(s)
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 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
else if ext = 'SX' then ans := '' // STUB: spectrum X
else if ext = 'SY' then ans := '' // STUB: spectrum Y
else if ext = 'SZ' then ans := '' // STUB: spectrum Z
else if ext = 'TA' then ans := ZZTA(s)
else if ext = 'TB' then ans := ZZTB(s)
else if ext = 'TF' then ans := '' // STUB: tone freq
else if ext = 'TH' then ans := '' // STUB: tone high
else if ext = 'TI' then ans := '' // STUB: tone input
else if ext = 'TL' then ans := '' // STUB: tone level
else if ext = 'TM' then ans := '' // STUB: tone mode
else if ext = 'TO' then ans := '' // STUB: tone on/off
else if ext = 'TP' then ans := '' // STUB: tone preset
else if ext = 'TS' then ans := '' // STUB: tone status
else if ext = 'TU' then ans := '' // STUB: tone update
else if ext = 'TV' then ans := '' // STUB: tone value
else if ext = 'TX' then ans := ZZTX(s)
else if ext = 'UA' then ans := '' // STUB: update A
else if ext = 'UP' then ans := ZZUP(s)
else if ext = 'US' then ans := ZZUS
else if ext = 'UT' then ans := ZZUT(s)
else if ext = 'UX' then ans := ZZUX(s)
else if ext = 'UY' then ans := ZZUY(s)
else if ext = 'VA' then ans := ZZVA(s)
else if ext = 'VB' then ans := ZZVB(s)
else if ext = 'VC' then ans := '' // STUB: voice compression
else if ext = 'VD' then ans := '' // STUB: VFO DSP
else if ext = 'VE' then ans := '' // STUB: VFO enable
else if ext = 'VF' then ans := ZZFA(s) // alias ZZFA
else if ext = 'VG' then ans := ZZVG(s)
else if ext = 'VH' then ans := '' // STUB: VFO high
else if ext = 'VI' then ans := '' // STUB: VFO input
else if ext = 'VJ' then ans := '' // STUB: VFO J
else if ext = 'VK' then ans := '' // STUB: VFO K
else if ext = 'VL' then ans := '' // STUB: VFO level
else if ext = 'VM' then ans := '' // STUB: VFO mode
else if ext = 'VN' then ans := ZZVN
else if ext = 'VO' then ans := '' // STUB: VFO on/off
else if ext = 'VP' then ans := '' // STUB: VFO preset
else if ext = 'VQ' then ans := '' // STUB: VFO Q
else if ext = 'VR' then ans := '' // STUB: VFO read
else if ext = 'VS' then ans := ZZVS(s)
else if ext = 'VT' then ans := '' // STUB: VFO TX
else if ext = 'VU' then ans := '' // STUB: VFO update
else if ext = 'VV' then ans := '' // STUB: VFO value
else if ext = 'VW' then ans := '' // STUB: VFO write
else if ext = 'VX' then ans := '' // STUB: VFO extended
else if ext = 'VY' then ans := '' // STUB: VFO Y
else if ext = 'VZ' then ans := '' // STUB: VFO Z
else if ext = 'WA' then ans := ZZWA(s)
else if ext = 'WB' then ans := ZZWB(s)
else if ext = 'WC' then ans := ZZWC(s)
else if ext = 'WD' then ans := ZZWD(s)
else if ext = 'WE' then ans := ZZWE(s)
else if ext = 'WF' then ans := ZZWF(s)
else if ext = 'WG' then ans := ZZWG(s)
else if ext = 'WH' then ans := ZZWH(s)
else if ext = 'WJ' then ans := ZZWJ(s)
else if ext = 'WK' then ans := ZZWK(s)
else if ext = 'WL' then ans := ZZWL(s)
else if ext = 'WM' then ans := ZZWM(s)
else if ext = 'WN' then ans := ZZWN(s)
else if ext = 'WO' then ans := ZZWO(s)
else if ext = 'WP' then ans := ZZWP(s)
else if ext = 'WQ' then ans := ZZWQ(s)
else if ext = 'WR' then ans := ZZWR(s)
else if ext = 'WS' then ans := ZZWS(s)
else if ext = 'WT' then ans := '' // STUB: waterfall T
else if ext = 'WU' then ans := '' // STUB: waterfall U
else if ext = 'WV' then ans := '' // STUB: waterfall V
else if ext = 'WW' then ans := '' // STUB: waterfall W
else if ext = 'XA' then ans := ZZXA(s)
else if ext = 'XC' then ans := ZZXC
else if ext = 'XD' then ans := '' // STUB: extended D
else if ext = 'XF' then ans := ZZXF(s)
else if ext = 'XH' then ans := '' // STUB: extended H
else if ext = 'XN' then ans := '' // STUB: extended N
else if ext = 'XO' then ans := '' // STUB: extended O
else if ext = 'XS' then ans := '' // STUB: extended S
else if ext = 'XT' then ans := '' // STUB: extended T
else if ext = 'XU' then ans := '' // STUB: extended U
else if ext = 'XV' then ans := '' // STUB: extended V
else if ext = 'YA' then ans := ZZYA(s)
else if ext = 'YB' then ans := ZZYB(s)
else if ext = 'YC' then ans := ZZYC(s)
else if ext = 'YR' then ans := ZZYR(s)
else if ext = 'ZA' then ans := ZZZA(s)
else if ext = 'ZB' then begin ZZZB; ans := ''; end
else if ext = 'ZD' then ans := ZZZD(s)
else if ext = 'ZE' then ans := ZZZE(s)
else if ext = 'ZM' then ans := ZZZM(s)
else if ext = 'ZN' then ans := '' // STUB: system N
else if ext = 'ZO' then ans := ZZZO(s)
else if ext = 'ZP' then ans := ZZZP(s)
else if ext = 'ZQ' then ans := ZZZQ(s)
else if ext = 'ZR' then ans := ZZZR(s)
else if ext = 'ZS' then ans := ZZZS(s)
else if ext = 'ZT' then ans := ZZZT(s)
else if ext = 'ZU' then ans := ZZZU(s)
else if ext = 'ZV' then ans := ZZZV(s)
else if ext = 'ZW' then ans := ZZZW(s)
else if ext = 'ZZ' then begin ZZZZ; ans := ''; end
else begin Result := CAT_ERROR; Exit; end;
if (ans = '') or (ans = CAT_ERROR) or (ans = CAT_EXEC_ERROR) then
Result := ans
else
Result := 'ZZ' + ext + s + ans + ';';
end;
{ ══════════════════════════════════════════════════════════════════════════
ZZ command implementations
══════════════════════════════════════════════════════════════════════════ }
function TCATEngine.ZZAA(const s: string): string;
// ZZAA: RX1 AF gain — alias ZZAG
begin Result := ZZAG(s); end;
function TCATEngine.ZZAB(const s: string): string;
// ZZAB: RX2 AF gain — STUB: no RX2
begin
if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZAC(const s: string): string;
// ZZAC: band selection (2-digit band index)
var idx: Integer;
begin
if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 10);
if Assigned(FCtx.DoBandByIndex) then FCtx.DoBandByIndex(idx);
Result := '';
end else if Length(s) = 0 then
Result := Pad(SafeGetBand, 2)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZAG(const s: string): string;
// ZZAG: AF gain 0100
var v: Integer;
begin
if Length(s) = 3 then begin
v := EnsureRange(StrToIntDef(s, 0), 0, 100);
if Assigned(FCtx.SetVolume) then FCtx.SetVolume(v);
Result := '';
end else if Length(s) = 0 then
Result := Pad(SafeGetVolume, 3)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZAI(const s: string): string;
// ZZAI: auto-information broadcast — STUB: not applicable for CAT-only mode
begin
if Length(s) = 1 then Result := ''
else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZAP(const s: string): string;
// ZZAP: TX power 0100
begin Result := ZZPC(s); end;
function TCATEngine.ZZAR(const s: string): string;
// ZZAR: RX1 AGC-T порог (RF gain). Формат: знак + 3 цифры (напр. "+020", "+120").
// Контроллер клампит в свой диапазон (20..120 dB).
var v: Integer;
begin
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
Result := SignedPad(SafeGetAGCTop, 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;
// ZZAS: sub-RX enable — STUB: no RX2
begin
if Length(s) = 1 then Result := ''
else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZAT(const s: string): string;
// ZZAT: antenna tuner — STUB: no ATU
begin
if Length(s) = 1 then Result := ''
else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZBA: string;
// ZZBA: sub-RX AF gain — STUB: no RX2
begin Result := '050'; end;
function TCATEngine.ZZBD: string;
// ZZBD: band down
begin
if Assigned(FCtx.DoBandDown) then FCtx.DoBandDown;
Result := '';
end;
function TCATEngine.ZZBE(const s: string): string;
// ZZBE: band enable — STUB: all bands always enabled
begin
if Length(s) = 3 then Result := ''
else if Length(s) = 2 then Result := '1'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZBI(const s: string): string;
// ZZBI: filter index (2-digit, 0-9)
var idx: Integer;
begin
if Length(s) = 2 then begin
idx := EnsureRange(StrToIntDef(s, 0), 0, 9);
if Assigned(FCtx.SetFilterIdx) then FCtx.SetFilterIdx(idx);
Result := '';
end else if Length(s) = 0 then
Result := Pad(SafeGetFilterIdx, 2)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZBM(const s: string): string;
// ZZBM: band mode — set mode for current band
begin Result := ZZMD(s); end;
function TCATEngine.ZZBS(const s: string): string;
// ZZBS: band select — same as ZZAC
begin Result := ZZAC(s); end;
function TCATEngine.ZZBU: string;
begin
if Assigned(FCtx.DoBandUp) then FCtx.DoBandUp;
Result := '';
end;
function TCATEngine.ZZCB(const s: string): string;
// ZZCB: compression bypass — STUB: TX DSP not implemented
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZCD(const s: string): string;
// ZZCD: compressor drive — STUB: TX DSP not implemented
begin
if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZCN(const s: string): string;
// ZZCN: CTUN (click-tune) для RX1 вкл/выкл.
begin
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;
function TCATEngine.ZZCS(const s: string): string;
// ZZCS: compressor status — STUB
begin
if Length(s) = 0 then Result := '0' else Result := CAT_ERROR;
end;
function TCATEngine.ZZDA(const s: string): string;
// ZZDA: diversity A — STUB: diversity not implemented
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZDB(const s: string): string;
// ZZDB: diversity B — STUB
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZDM(const s: string): string;
// ZZDM: display mode — STUB: no separate display mode setting
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZDN(const s: string): string;
// ZZDN: drive level — alias ZZPC
begin Result := ZZPC(s); end;
function TCATEngine.ZZDU: string;
// ZZDU: display units update — STUB
begin Result := ''; end;
function TCATEngine.ZZEM(const s: string): string;
// ZZEM: error message — returns error code info (Verbose mode)
begin Result := CAT_ERROR; end;
function TCATEngine.ZZEA(const s: string): string;
// ZZEA: RX EQ profile name — STUB: no EQ
begin
if Length(s) > 0 then Result := '' else Result := ''; // no EQ
end;
function TCATEngine.ZZEB(const s: string): string;
// ZZEB: TX EQ profile name — STUB: TX DSP not implemented
begin
if Length(s) > 0 then Result := '' else Result := '';
end;
function TCATEngine.ZZFA(const s: string): string;
// ZZFA: VFO A frequency (11 digits, Hz)
begin
if Length(s) = 11 then begin
if Assigned(FCtx.SetVfoA) then FCtx.SetVfoA(StrToFreq(s));
Result := '';
end else if Length(s) = 0 then
Result := FreqToStr(SafeGetVfoA)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZFB(const s: string): string;
// ZZFB: VFO B frequency
begin
if Length(s) = 11 then begin
if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(StrToFreq(s));
Result := '';
end else if Length(s) = 0 then
Result := FreqToStr(SafeGetVfoB)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZFD(const s: string): string;
// ZZFD: FM deviation — STUB: FM deviation not configurable
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '1'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZFI(const s: string): string;
// ZZFI: filter index (2-digit)
begin Result := ZZBI(s); end;
function TCATEngine.ZZFL(const s: string): string;
// ZZFL: filter low cut (signed Hz, e.g. "-0150"). Работает, если контекст умеет
// кромки (слайс-CAT); у главного приёмника фильтр индексный — заглушка.
begin
if Length(s) >= 4 then begin
if Assigned(FCtx.SetFilterLow) then FCtx.SetFilterLow(StrToIntDef(s, 0));
Result := '';
end else if Length(s) = 0 then begin
if Assigned(FCtx.GetFilterLow) then Result := SignedPad(FCtx.GetFilterLow, 4)
else if SafeGetMode in [MODE_CWL, MODE_CWU] then Result := '-0500'
else Result := '-1500';
end else
Result := CAT_ERROR;
end;
function TCATEngine.ZZFH(const s: string): string;
// ZZFH: filter high cut (signed Hz) — см. ZZFL.
begin
if Length(s) >= 4 then begin
if Assigned(FCtx.SetFilterHigh) then FCtx.SetFilterHigh(StrToIntDef(s, 0));
Result := '';
end else if Length(s) = 0 then begin
if Assigned(FCtx.GetFilterHigh) then Result := SignedPad(FCtx.GetFilterHigh, 4)
else if SafeGetMode in [MODE_CWL, MODE_CWU] then Result := '+0500'
else Result := '+3000';
end else
Result := CAT_ERROR;
end;
function TCATEngine.ZZFM: string;
// ZZFM: filter mode — read-only, returns current filter index as string
begin
Result := Pad(SafeGetFilterIdx, 2);
end;
function TCATEngine.ZZFT(const s: string): string;
// ZZFT: TX frequency — same as ZZFA (no split yet)
begin Result := ZZFA(s); end;
function TCATEngine.ZZGA(const s: string): string;
// ZZGA: GUID add (TCP client ID management) — handled by TCP server layer
// If called from serial, just acknowledge
begin
if Length(s) = 36 then Result := s
else Result := CAT_ERROR;
end;
function TCATEngine.ZZGR(const s: string): string;
// ZZGR: GUID remove (TCP client ID) — handled by TCP server layer
begin
if Length(s) = 36 then Result := s
else Result := CAT_ERROR;
end;
function TCATEngine.ZZGT(const s: string): string;
// ZZGT: AGC mode (1 char: 0=FAST,1=MED,2=SLOW,3=LONG,4=OFF) → 3-digit response
var m: Integer;
begin
if Length(s) = 1 then begin
m := EnsureRange(StrToIntDef(s, 0), 0, 4);
if Assigned(FCtx.SetAGCMode) then FCtx.SetAGCMode(m);
Result := '';
end else if Length(s) = 0 then
Result := Pad(SafeGetAGCMode, 3)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZID: string;
begin Result := CAT_ID_NUM; end;
function TCATEngine.ZZIF(const s: string): string;
// ZZIF: IF — STUB: secondary interface (returns same as IF)
begin
if Length(s) = 0 then Result := CmdIF
else Result := CAT_ERROR;
end;
function TCATEngine.ZZIO: string;
// ZZIO: I/O status — STUB
begin Result := '0'; end;
function TCATEngine.ZZIS(const s: string): string;
// ZZIS: initial setup flag — STUB
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '1'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZKM(const s: string): string;
// ZZKM: CW key mode — STUB: no CW keyer
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZKS(const s: string): string;
// ZZKS: CW speed (WPM) — STUB: no CW keyer
begin
if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '025'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZKY(const s: string): string;
// ZZKY: CW text to send — STUB: no CW keyer implemented
begin
Result := '0'; // buffer not full
end;
function TCATEngine.ZZMA(const s: string): string;
// ZZMA: RX1 mode
begin Result := ZZMD(s); end;
function TCATEngine.ZZMB(const s: string): string;
// ZZMB: RX2 mode — STUB: no RX2; returns same as RX1
begin Result := ZZMD(s); end;
function TCATEngine.ZZMD(const s: string): string;
// ZZMD: mode (1 digit Kenwood code, same as MD)
var k: Integer;
begin
if Length(s) = 1 then begin
k := StrToIntDef(s, 0);
if (k >= 1) and Assigned(FCtx.SetMode) then FCtx.SetMode(KToMode(k));
Result := '';
end else if Length(s) = 0 then
Result := IntToStr(ModeToK(SafeGetMode))
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZMG(const s: string): string;
// ZZMG: microphone gain 070 (Thetis scale) — STUB: TX DSP not implemented
begin
if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZML: string;
// ZZML: mode list (returns all mode names)
begin Result := 'LSB USB DSB CWL CWU FM AM SAM DIGUDIGLWFM DMR FMRAW'; end;
function TCATEngine.ZZMN(const s: string): string;
// ZZMN: mode name — returns mode name for given mode number.
// Индекс режима двузначный (MODE_MIN..MODE_MAX), но односимвольный запрос
// продолжаем принимать — старые клиенты шлют одну цифру.
var k: Integer;
names: array[0..CAT_MODE_COUNT-1] of string;
begin
names[MODE_LSB]:='LSB'; names[MODE_USB]:='USB';
names[MODE_DSB]:='DSB'; names[MODE_CWL]:='CWL';
names[MODE_CWU]:='CWU'; names[MODE_FM]:='FM';
names[MODE_AM]:='AM'; names[MODE_SAM]:='SAM';
names[MODE_DIGU]:='DIGU'; names[MODE_DIGL]:='DIGL';
names[MODE_WFM]:='WFM'; names[MODE_DMR]:='DMR';
names[MODE_FMRAW]:='FM RAW';
if (Length(s) = 1) or (Length(s) = 2) then begin
k := EnsureRange(StrToIntDef(s, MODE_MIN), MODE_MIN, MODE_MAX);
Result := names[k];
end else Result := CAT_ERROR;
end;
function TCATEngine.ZZMO(const s: string): string;
// ZZMO: monitor on/off — STUB: no TX monitor
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZMV: string;
// ZZMV: mode value — returns current ewsdr mode index (MODE_MIN..MODE_MAX)
begin Result := Pad(SafeGetMode, 2); end;
function TCATEngine.ZZMY: string;
// ZZMY: mode Y — STUB
begin Result := ''; end;
function TCATEngine.ZZNA(const s: string): string;
// ZZNA: noise blanker A (NB1) on/off
var v: Integer;
begin
if Length(s) = 1 then begin
v := StrToIntDef(s, 0);
if Assigned(FCtx.SetNBMode) then FCtx.SetNBMode(v);
Result := '';
end else if Length(s) = 0 then
Result := IntToStr(EnsureRange(SafeGetNBMode,0,1))
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZNB(const s: string): string;
// ZZNB: noise blanker mode (extended, 0-4)
var v: Integer;
begin
if Length(s) = 1 then begin
v := EnsureRange(StrToIntDef(s,0),0,4);
if Assigned(FCtx.SetNBMode) then FCtx.SetNBMode(v);
Result := '';
end else if Length(s) = 0 then
Result := IntToStr(SafeGetNBMode)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZNL(const s: string): string;
// ZZNL: NB level — STUB: NB level not configurable
begin
if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZNN(const s: string): string;
// ZZNN: notch (manual) on/off — STUB: no manual notch
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZNR(const s: string): string;
// ZZNR: noise reduction on/off (0=off, 1-4=NR mode)
var v: Integer;
begin
if Length(s) = 1 then begin
v := EnsureRange(StrToIntDef(s,0),0,4);
if Assigned(FCtx.SetNRMode) then FCtx.SetNRMode(v);
Result := '';
end else if Length(s) = 0 then
Result := IntToStr(SafeGetNRMode)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZNS(const s: string): string;
// ZZNS: spectral noise blanker (SNB) on/off
begin
if Length(s) = 1 then begin
if Assigned(FCtx.SetSNBEnabled) then FCtx.SetSNBEnabled(s = '1');
Result := '';
end else if Length(s) = 0 then
Result := B2C(SafeGetSNB)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZNT(const s: string): string;
// 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
begin Result := ZZAG(s); end;
function TCATEngine.ZZOB(const s: string): string;
// ZZOB: RX2 output level — STUB: no RX2
begin
if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZPC(const s: string): string;
// ZZPC: TX power 0100 → DriveLevel
var v: Integer;
begin
if Length(s) = 3 then begin
v := EnsureRange(StrToIntDef(s,0),0,100);
if Assigned(FCtx.SetDriveLevel) then FCtx.SetDriveLevel(v);
Result := '';
end else if Length(s) = 0 then
Result := Pad(SafeGetDriveLevel, 3)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZPD: string;
// ZZPD: power default — return nominal 100
begin Result := '100'; end;
function TCATEngine.ZZPO(const s: string): string;
// ZZPO: power on/off — STUB: always on when running
begin
if Length(s) = 1 then Result := ''
else if Length(s) = 0 then Result := B2C(SafeGetRunning)
else Result := CAT_ERROR;
end;
function TCATEngine.ZZPS(const s: string): string;
// ZZPS: power status
begin
if Length(s) = 1 then Result := ''
else if Length(s) = 0 then Result := B2C(SafeGetRunning)
else Result := CAT_ERROR;
end;
function TCATEngine.ZZQM: string;
// ZZQM: current mode query
begin Result := IntToStr(ModeToK(SafeGetMode)); end;
function TCATEngine.ZZQR: string;
// ZZQR: quick recall — STUB
begin Result := ''; end;
function TCATEngine.ZZQS: string;
// ZZQS: quick store — STUB
begin Result := ''; end;
function TCATEngine.ZZRA(const s: string): string;
// ZZRA: RIT A value — STUB: no RIT
begin
if Length(s) = 5 then Result := '' else if Length(s) = 0 then Result := '+0000'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZRC: string;
begin Result := ''; end; // clear RIT — STUB
function TCATEngine.ZZRD(const s: string): string;
// ZZRD: tune/RIT down
begin
if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown;
Result := '';
end;
function TCATEngine.ZZRL(const s: string): string;
// ZZRL: RF gain — STUB: no RF gain control
begin
if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '100'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZRU(const s: string): string;
// ZZRU: tune/RIT up
begin
if Assigned(FCtx.DoTuneUp) then FCtx.DoTuneUp;
Result := '';
end;
function TCATEngine.ZZRV: string;
// ZZRV: software version
begin Result := '001000'; end; // version 0.1.0
function TCATEngine.ZZRX(const s: string): string;
// ZZRX: switch to receive
begin
if Assigned(FCtx.SetTransmitting) then FCtx.SetTransmitting(False);
if Length(s) = 0 then Result := B2C(not SafeGetTX)
else Result := '';
end;
function TCATEngine.ZZSA: string;
// ZZSA: VFO tune down (maps to step down)
begin
if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown;
Result := '';
end;
function TCATEngine.ZZSB: string;
// ZZSB: VFO tune up (maps to step up)
begin
if Assigned(FCtx.DoTuneUp) then FCtx.DoTuneUp;
Result := '';
end;
function TCATEngine.ZZSM(const s: string): string;
// ZZSM: S-meter (0=RX1, 1=RX2)
// Response: 3-digit 000-260 (calculated as (dBm+140)*2)
var sm: Integer;
begin
if (Length(s) = 1) and ((s = '0') or (s = '1')) then begin
sm := SMeterRaw(SafeGetSMeter);
Result := Pad(sm, 3);
end else
Result := CAT_ERROR;
end;
function TCATEngine.ZZSO(const s: string): string;
// ZZSO: squelch on/off (расширенный статус шумоподавителя). 0=off, иначе on.
begin
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 — 1=TX на VFO-B, 0=TX на VFO-A. TX-частоту радио берёт из
// ActiveTXFreqHz (учитывает split), поэтому достаточно выставить флаг.
begin
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 threshold (3 digits, 000100 %). Уровень>0 включает шумоподавитель,
// 000 — открывает (выключает). Маппится на FM-squelch контроллера.
var v: Integer;
begin
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;
// ZZSR: spectrum reference level (dBm, signed 4 chars e.g. "-020")
begin
if Length(s) = 4 then Result := '' // STUB: spec ref not settable via CAT
else if Length(s) = 0 then Result := '-020'
else Result := CAT_ERROR;
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: шаг настройки вверх (как колесо мыши) — сдвигает активный VFO вверх.
begin
if Assigned(FCtx.DoTuneUp) then FCtx.DoTuneUp;
Result := '';
end;
function TCATEngine.ZZTA(const s: string): string;
// ZZTA: CTCSS on/off (1=on, 0=off) для FM-передачи.
begin
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 index (2 digits, 0-based 00..37 в таблице CTCSS_TONES).
begin
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;
// ZZTX: TX on/off (0=RX, 1=TX, 2=TUNE)
begin
if Length(s) = 1 then begin
if Assigned(FCtx.SetTransmitting) then FCtx.SetTransmitting(s <> '0');
Result := '';
end else if Length(s) = 0 then
Result := B2C(SafeGetTX)
else
Result := CAT_ERROR;
end;
function TCATEngine.ZZUP(const s: string): string;
// ZZUP: xPA on/off — STUB: no external PA control
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZUS: string;
// ZZUS: two-tone test — STUB: TX DSP not implemented
begin Result := ''; end;
function TCATEngine.ZZUT(const s: string): string;
// ZZUT: TX update — STUB
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZUX(const s: string): string;
// ZZUX: VFO A lock — STUB
begin
if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZUY(const s: string): string;
// ZZUY: VFO B lock — STUB
begin Result := ZZUX(s); end;
function TCATEngine.ZZVA(const s: string): string;
// ZZVA: swap VFO A↔B
var tmp: Double;
begin
if Length(s) = 0 then begin
if Assigned(FCtx.GetVfoA) and Assigned(FCtx.GetVfoB) and
Assigned(FCtx.SetVfoA) and Assigned(FCtx.SetVfoB) then begin
tmp := FCtx.GetVfoA;
FCtx.SetVfoA(FCtx.GetVfoB);
FCtx.SetVfoB(tmp);
end;
Result := '';
end else Result := CAT_ERROR;
end;
function TCATEngine.ZZVB(const s: string): string;
// ZZVB: copy VFO A to B — STUB: needs explicit operation
begin
if Length(s) = 0 then begin
if Assigned(FCtx.GetVfoA) and Assigned(FCtx.SetVfoB) then
FCtx.SetVfoB(FCtx.GetVfoA);
Result := '';
end else Result := CAT_ERROR;
end;
function TCATEngine.ZZVG(const s: string): string;
// ZZVG: volume/gain — alias ZZAG
begin Result := ZZAG(s); end;
function TCATEngine.ZZVN: string;
// ZZVN: software version string
begin Result := 'EWSDR 0.1.0'; end;
function TCATEngine.ZZVS(const s: string): string;
// ZZVS: VFO split — STUB
begin Result := ZZSP(s); end;
// WA..WS: Waterfall settings — STUB: waterfall not controllable via CAT
// These are Thetis-specific waterfall display controls
function TCATEngine.ZZWA(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWB(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWC(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWD(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWE(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWF(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWG(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWH(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWJ(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWK(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWL(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWM(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWN(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWO(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWP(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWQ(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWR(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZWS(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
// XA, XC, XF: extended controls — STUB: Thetis-specific hardware
function TCATEngine.ZZXA(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZXC: string;
begin Result := '0'; end;
function TCATEngine.ZZXF(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
// YA..YR: Y-axis / spectrum controls — STUB
function TCATEngine.ZZYA(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZYB(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZYC(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZYR(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
// ZA..ZZ: System commands — STUB (most are Thetis-specific internal controls)
function TCATEngine.ZZZA(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZZB: string; begin Result := ''; end;
function TCATEngine.ZZZD(const s: string): string;
// ZZZD: Andromeda — энкодер VFO повёрнут вниз на nn шагов (write-only).
begin
if Length(s) > 0 then begin
if Assigned(FCtx.OnPanelVfoStep) then FCtx.OnPanelVfoStep(-StrToIntDef(s, 0));
Result := '';
end else
Result := CAT_ERROR;
end;
function TCATEngine.ZZZE(const s: string): string;
// ZZZE: Andromeda — поворот энкодера. Кодировка Thetis: верхние 2 цифры — номер
// энкодера (01..20 = +шаг, 51..70 = −шаг), нижняя цифра — величина шага.
var raw, enc, step: Integer;
begin
if Length(s) > 0 then begin
raw := StrToIntDef(s, 0);
step := raw mod 10;
enc := raw div 10;
if (enc >= 1) and (enc <= 20) then begin
if Assigned(FCtx.OnPanelEncoder) then FCtx.OnPanelEncoder(enc - 1, step);
end else if (enc >= 51) and (enc <= 70) then begin
if Assigned(FCtx.OnPanelEncoder) then FCtx.OnPanelEncoder(enc - 51, -step);
end;
Result := '';
end else
Result := CAT_ERROR;
end;
function TCATEngine.ZZZM(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZZP(const s: string): string;
// ZZZP: Andromeda — нажатие кнопки (write-only). Кодировка: nn*10 + state,
// state: 0=отпущена, 1=нажата, 2=long-press. nn — номер кнопки (1-based).
var raw, btn: Integer; state, long: Boolean;
begin
if Length(s) > 0 then begin
raw := StrToIntDef(s, 0);
state := (raw mod 10) = 1;
long := (raw mod 10) = 2;
btn := raw div 10;
if Assigned(FCtx.OnPanelButton) then FCtx.OnPanelButton(btn - 1, state, long);
Result := '';
end else
Result := CAT_ERROR;
end;
function TCATEngine.ZZZQ(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZZR(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZZS(const s: string): string;
// ZZZS: Andromeda — панель сообщает версию HW/SW (write-only, рукопожатие).
begin
if Length(s) > 0 then begin
if Assigned(FCtx.OnPanelVersion) then FCtx.OnPanelVersion(StrToIntDef(s, 0));
Result := '';
end else
Result := CAT_ERROR;
end;
function TCATEngine.ZZZT(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZZU(const s: string): string;
// ZZZU: Andromeda — энкодер VFO повёрнут вверх на nn шагов (write-only).
begin
if Length(s) > 0 then begin
if Assigned(FCtx.OnPanelVfoStep) then FCtx.OnPanelVfoStep(StrToIntDef(s, 0));
Result := '';
end else
Result := CAT_ERROR;
end;
function TCATEngine.ZZZV(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZZW(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
function TCATEngine.ZZZZ: string;
// ZZZZ: system reset — STUB: would require restarting DSP engine
begin Result := ''; end;
function TCATEngine.ZZZO(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
end.