unit CATEngine; { CATEngine.pas — CAT command engine (Kenwood + Thetis ZZ* extensions). Совместимость с Thetis/PowerSDR: все команды из CATParser.cs + CATCommands.cs реализованы. Команды, для которых в ewsdr ещё нет функционала, возвращают заглушку и сопровождены комментарием "STUB: <причина>". Формат команд: SET: ; (suffix = значение) GET: ; (suffix пустой) Resp: ; 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; const CAT_ERROR = '?;'; CAT_EXEC_ERROR = 'E;'; CAT_ID_NUM = '019'; // TS-2000 compatible // Mode mapping ewsdr → CAT MODE_LSB = 0; MODE_USB = 1; MODE_DSB = 2; 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; 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; // 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 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.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_AM: Result := 5; MODE_SAM: Result := 5; // treated as AM MODE_CWL: Result := 7; 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_USB; // FSK/RTTY → USB 7: Result := MODE_CWL; 9: Result := MODE_LSB; // FSK-R → LSB 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 0–255 (Kenwood) ↔ 0–100 (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: 0000–0015 → S0–S9+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=0/1 RX, lll=000–255). GET: . // Шкала 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 0–100 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 0–100 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 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; // 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. "-150") // STUB: ewsdr uses indexed filters, not arbitrary Hz cutoffs begin if Length(s) >= 4 then Result := '' // accept but ignore — STUB else if Length(s) = 0 then begin 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) // STUB: ewsdr uses indexed filters, not arbitrary Hz cutoffs begin if Length(s) >= 4 then Result := '' // accept but ignore — STUB else if Length(s) = 0 then begin 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 0–70 (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 '; end; function TCATEngine.ZZMN(const s: string): string; // ZZMN: mode name — returns mode name for given mode number var k: Integer; names: array[0..7] of string; begin names[0]:='LSB'; names[1]:='USB'; names[2]:='DSB'; names[3]:='CWL'; names[4]:='CWU'; names[5]:='FM'; names[6]:='AM'; names[7]:='SAM'; if Length(s) = 1 then begin k := EnsureRange(StrToIntDef(s,0),0,7); 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 mode index (ewsdr 0-7) 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 0–100 → 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, 000–100 %). Уровень>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.