Files
ewsdr/CATEngine.pas
T
ew8bakandClaude Opus 5 fe6fb9e292 fix(cat): ZZVB копировал VFO при опросе, KY писал в сокет без MSG_NOSIGNAL
Три замечания ревью в CAT-слое.

ZZVB. Команда была переписана под заглушку VAC (усиление приёма в
виртуальном кабеле, которого у нас нет), но заменили только комментарий —
тело осталось прежним, от старого ошибочного алиаса «обмен VFO». А ветка с
пустым суффиксом — это ФОРМА ОПРОСА: логгер, который просто спрашивает
«ZZVB;», молча затирал VFO B частотой VFO A, то есть терял сплит оператора и
даже не получал в ответ ошибку. Соседи по этому же диффу (ZZVA, ZZVG, ZZVS,
ZZAA, ZZAP, ZZBI, ZZBM, ZZDN, ZZMA, ZZMV, ZZQM, ZZOA, ZZPO, ZZSR) тело
получили, ZZVB — нет. Теперь это строчная заглушка в таблице, рядом с ZZVC и
ZZVD, которые про тот же несуществующий VAC.

ZZEB. Число полос эквалайзера принималось любое из 0..10 и уходило прямо в
TTXSettings.EQNumBands, то есть в СОХРАНЯЕМЫЙ TX-профиль. Потребители знают
ровно два случая: WDSPEngine.PushTXEQProfile ветвится на «3», редактор в
настройках — на 3 и 10. «ZZEB000…;» записывал ноль полос, всё прочее тихо
играло по полной 11-узловой кривой с чужой подписью. Принимаем только 3 и 10.

CATTcp.SendStr. Писал в сокет голым fpSend/send с флагами 0. В этой же ветке
WebUtils.SockSend получил MSG_NOSIGNAL ровно потому, что запись в закрытый
клиентом сокет иначе приходит как SIGPIPE, а он по умолчанию убивает процесс
целиком; обработчика сигнала в дереве нет. CAT про это забыли, а добавленный
здесь же цикл дозаписи расширил окно: длинный ответ (IF, ZZEB, список
режимов) уходит теперь несколькими send, и каждый может застать клиента уже
ушедшим. Пишем через WebUtils.SockSend — заодно ушла платформенная развилка.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 10:44:14 +03:00

2880 lines
124 KiB
ObjectPascal
Raw 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;
// Границы TX/CW-полей. Совпадают с контролами вкладок Transmit и CW: движок
// клампует сам, потому что по CAT прилетает что угодно, а контроллер эти
// поля принимает записью целиком и уже не знает, кто их правил.
MIC_GAIN_MIN = -20; MIC_GAIN_MAX = 60; // дБ
TX_FILT_LO_MIN = 0; TX_FILT_LO_MAX = 8000; // Гц
TX_FILT_HI_MIN = 100; TX_FILT_HI_MAX = 12000; // Гц
COMP_GAIN_MAX = 20; // дБ
EQ_GAIN_MIN = -20; EQ_GAIN_MAX = 20; // дБ
CW_PITCH_MIN = 200; CW_PITCH_MAX = 1200; // Гц
CW_HANG_MAX = 2000; // мс
CW_WPM_MIN = 5; CW_WPM_MAX = 60;
CW_TEXT_FIELD = 25; // ширина поля текста KY/ZZKY
FM_DEV_NARROW = 2500; FM_DEV_WIDE = 5000; // Гц (ZZFD — радиокнопка)
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;
// TX-эквалайзер (ZZEB): [0] — преамп, 1..10 — полосы, дБ. Формат провода
// (счётчик полос + знаковые тройки) знает движок, состав записи — адаптер.
TCATEQGains = array[0..10] of Integer;
TCATGetEQFunc = function(out Gains: TCATEQGains): Integer of object; // → число полос
TCATSetEQProc = procedure(NumBands: Integer; const Gains: TCATEQGains) 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;
// Телеграф (KS/KY/ZZKS/ZZKY/ZZKM). Скорость — WPM 5..60; SendCWText ставит
// текст в очередь программной передачи, CWTextBusy = «очередь занята».
GetCWSpeed: TCATGetIntFunc;
SetCWSpeed: TCATSetIntProc;
GetCWKeyerMode: TCATGetIntFunc; // 0=прямой ключ, 1=iambic A, 2=iambic B
SetCWKeyerMode: TCATSetIntProc;
SendCWText: procedure(const S: string) of object;
CWTextBusy: TCATGetBoolFunc;
// Телеграф, продолжение: тон приёма/сайдтона (ZZCL), break-in прошивки
// (ZZCB) и его hang-time (ZZCD), сайдтон активного источника манипуляции
// (ZZCM — у Thetis это «monitor DISABLE», инверсия делается в движке).
GetCWPitch: TCATGetIntFunc; // Гц
SetCWPitch: TCATSetIntProc;
GetCWBreakIn: TCATGetBoolFunc;
SetCWBreakIn: TCATSetBoolProc;
GetCWHangTime: TCATGetIntFunc; // мс
SetCWHangTime: TCATSetIntProc;
GetCWMonitor: TCATGetBoolFunc;
SetCWMonitor: TCATSetBoolProc;
// ── TX-тракт (вкладка Transmit / активный TX-профиль) ──
GetMicGain: TCATGetIntFunc; // дБ (у EWSDR -20..+60)
SetMicGain: TCATSetIntProc;
GetTXFilterLow: TCATGetIntFunc; // Гц
SetTXFilterLow: TCATSetIntProc;
GetTXFilterHigh: TCATGetIntFunc; // Гц
SetTXFilterHigh: TCATSetIntProc;
GetCompOn: TCATGetBoolFunc; // речевой компрессор (WDSP COMP)
SetCompOn: TCATSetBoolProc;
GetCompGain: TCATGetIntFunc; // дБ 0..20
SetCompGain: TCATSetIntProc;
GetTXEQOn: TCATGetBoolFunc;
SetTXEQOn: TCATSetBoolProc;
GetTXEQ: TCATGetEQFunc;
SetTXEQ: TCATSetEQProc;
GetTUNLevel: TCATGetIntFunc; // мощность настройки, 0..100 %
SetTUNLevel: TCATSetIntProc;
GetFMDeviation: TCATGetIntFunc; // девиация передачи, Гц
SetFMDeviation: TCATSetIntProc;
GetTXProfile: TCATGetIntFunc; // индекс активного профиля
SetTXProfile: TCATSetIntProc;
GetTXProfileCount: TCATGetIntFunc;
// MON: у EWSDR монитор передачи — это self-monitor даунлинка (DUP + RX MUTE
// off), уровень у него свой, отдельный от RX-громкости.
GetMonitorOn: TCATGetBoolFunc;
SetMonitorOn: TCATSetBoolProc;
GetMonitorLevel: TCATGetIntFunc; // 0..100
SetMonitorLevel: TCATSetIntProc;
GetTuning: TCATGetBoolFunc; // кнопка TUN
SetTuning: TCATSetBoolProc;
GetTwoTone: TCATGetBoolFunc; // 2TON
SetTwoTone: TCATSetBoolProc;
GetPureSignal: TCATGetBoolFunc; // PS-A
SetPureSignal: TCATSetBoolProc;
DoPSSingleCal: TCATProc; // ZZUS — одиночная калибровка PS
// ── Приёмник и сервис ──
GetMuted: TCATGetBoolFunc; // кнопка MUT (ZZMA)
SetMuted: TCATSetBoolProc;
GetAtten: TCATGetIntFunc; // шаговый аттенюатор ADC, дБ (ZZRX)
SetAtten: TCATSetIntProc;
GetSupplyVolts: TCATGetDoubleFunc; // напряжение питания PA (ZZRV); <0 = нет данных
DoVfoSwap: TCATProc; // обмен VFO A↔B (ZZVS)
GetMemoryCount: TCATGetIntFunc; // число записей памяти каналов (ZZMV)
SendCWMacro: TCATSetIntProc; // запустить CW-макрос (сообщение) номер n (ZZKM)
// 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 SafeGetCWSpeed: 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;
// TX-тракт / телеграф
function SafeGetCWKeyerMode: Integer;
function SafeGetCWPitch: Integer;
function SafeGetCWHangTime: Integer;
function SafeGetMicGain: Integer;
function SafeGetTXFilterLow: Integer;
function SafeGetTXFilterHigh: Integer;
function SafeGetCompGain: Integer;
function SafeGetTUNLevel: Integer;
function SafeGetFMDeviation: Integer;
function SafeGetTXProfile: Integer;
function SafeGetMonitorLevel: Integer;
function SafeGetBool(const F: TCATGetBoolFunc): Boolean;
// Общий разбор «1 символ 0/1» для двухпозиционных команд: SET дёргает Setter
// (если он есть), GET отдаёт Getter. Пустой Setter = команда только читает.
function OnOff(const s: string; const Getter: TCATGetBoolFunc;
const Setter: TCATSetBoolProc): string;
// То же для числового поля фиксированной ширины с клампом.
function NumField(const s: string; Width, Lo, Hi: Integer;
const Getter: TCATGetIntFunc;
const Setter: TCATSetIntProc): string;
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 ZZOS(const s: string): string;
function ZZOT(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 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;
// ── TX-тракт и телеграф (проброшены в контроллер, см. doc/CAT_STATUS.md) ──
function ZZCI(const s: string): string;
function ZZCL(const s: string): string;
function ZZCM(const s: string): string;
function ZZET(const s: string): string;
function ZZLI(const s: string): string;
function ZZPK(const s: string): string;
function ZZPL(const s: string): string;
function ZZTH(const s: string): string;
function ZZTL(const s: string): string;
function ZZTM(const s: string): string;
function ZZTO(const s: string): string;
function ZZTP(const s: string): string;
function ZZTU(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;
function IsParamless(const Pfx: string): Boolean;
// Кенвудовские команды, у которых и SET, и GET идут с пустым суффиксом
// (nsetparms=0 / ngetparms=0 в CATStructs.xml). Любой хвост у них — ошибка
// формата, а не значение.
const
NoArg: array[0..9] of string = (
'BD', 'BU', 'DN', 'UP', // диапазон и шаг настройки
'TX', 'RX', // передача/приём — тут цена ошибки самая высокая
'QI', 'RC', // быстрая память, сброс RIT
'ID', 'IF'); // только чтение, аргумента не бывает
var i: Integer;
begin
for i := Low(NoArg) to High(NoArg) do
if Pfx = NoArg[i] then Exit(True);
Result := False;
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.SafeGetCWKeyerMode: Integer;
begin
if Assigned(FCtx.GetCWKeyerMode) then Result := FCtx.GetCWKeyerMode else Result := 0;
end;
function TCATEngine.SafeGetCWPitch: Integer;
begin
if Assigned(FCtx.GetCWPitch) then Result := FCtx.GetCWPitch else Result := 600;
end;
function TCATEngine.SafeGetCWHangTime: Integer;
begin
if Assigned(FCtx.GetCWHangTime) then Result := FCtx.GetCWHangTime else Result := 300;
end;
function TCATEngine.SafeGetMicGain: Integer;
begin
if Assigned(FCtx.GetMicGain) then Result := FCtx.GetMicGain else Result := 10;
end;
function TCATEngine.SafeGetTXFilterLow: Integer;
begin
if Assigned(FCtx.GetTXFilterLow) then Result := FCtx.GetTXFilterLow else Result := 100;
end;
function TCATEngine.SafeGetTXFilterHigh: Integer;
begin
if Assigned(FCtx.GetTXFilterHigh) then Result := FCtx.GetTXFilterHigh else Result := 3100;
end;
function TCATEngine.SafeGetCompGain: Integer;
begin
if Assigned(FCtx.GetCompGain) then Result := FCtx.GetCompGain else Result := 0;
end;
function TCATEngine.SafeGetTUNLevel: Integer;
begin
if Assigned(FCtx.GetTUNLevel) then Result := FCtx.GetTUNLevel else Result := 10;
end;
function TCATEngine.SafeGetFMDeviation: Integer;
begin
if Assigned(FCtx.GetFMDeviation) then Result := FCtx.GetFMDeviation else Result := 5000;
end;
function TCATEngine.SafeGetTXProfile: Integer;
begin
if Assigned(FCtx.GetTXProfile) then Result := FCtx.GetTXProfile else Result := 0;
end;
function TCATEngine.SafeGetMonitorLevel: Integer;
begin
if Assigned(FCtx.GetMonitorLevel) then Result := FCtx.GetMonitorLevel else Result := 0;
end;
function TCATEngine.SafeGetBool(const F: TCATGetBoolFunc): Boolean;
begin
if Assigned(F) then Result := F else Result := False;
end;
function TCATEngine.OnOff(const s: string; const Getter: TCATGetBoolFunc;
const Setter: TCATSetBoolProc): string;
// Двухпозиционная команда «<PREFIX>0/1;». Формы SET и GET у всех одинаковы —
// держим разбор в одном месте, чтобы тела команд остались про смысл.
begin
if Length(s) = 0 then Exit(B2C(SafeGetBool(Getter)));
if (s <> '0') and (s <> '1') then Exit(CAT_ERROR);
if Assigned(Setter) then Setter(s = '1');
Result := '';
end;
function TCATEngine.NumField(const s: string; Width, Lo, Hi: Integer;
const Getter: TCATGetIntFunc; const Setter: TCATSetIntProc): string;
// Числовое поле фиксированной ширины: SET клампует в [Lo..Hi] (как Thetis —
// выход за границы не ошибка, а подрезка), GET дополняет нулями слева.
var v: Integer;
begin
if Length(s) = 0 then
begin
if Assigned(Getter) then v := Getter else v := Lo;
Exit(Pad(EnsureRange(v, Lo, Hi), Width));
end;
if Length(s) <> Width then Exit(CAT_ERROR);
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
if Assigned(Setter) then Setter(EnsureRange(v, Lo, Hi));
Result := '';
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);
// Команды без параметров: эталон отбраковывает лишний суффикс в парсере,
// по таблице ширин, ДО вызова команды. Таблицы у нас нет — держим список
// здесь, иначе «TXfoo;» дошёл бы до CmdTX и поднял передачу.
if (sfx <> '') and IsParamless(pfx) then Exit(CAT_ERROR);
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 Result := CmdRD(sfx)
else if pfx = 'RT' then Result := CmdRT(sfx)
else if pfx = 'RU' then Result := CmdRU(sfx)
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 — обёртка над ZZTA, как и в эталоне.
begin
Result := ZZTA(s);
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: составной статус — 35 символов ответа (как у Thetis; «37 байт» из
// прежнего комментария — это вся посылка вместе с префиксом и ';').
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.SafeGetCWSpeed: Integer;
begin
if Assigned(FCtx.GetCWSpeed) then Result := FCtx.GetCWSpeed else Result := 20;
end;
function TCATEngine.CmdKS(const s: string): string;
// KS: CW keying speed (3 digits, WPM).
var v: Integer;
begin
if Length(s) = 3 then
begin
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
if Assigned(FCtx.SetCWSpeed) then FCtx.SetCWSpeed(v);
Result := '';
end
else if Length(s) = 0 then
Result := Format('%.3d', [SafeGetCWSpeed])
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdKY(const s: string): string;
// KY: передать текст телеграфом. Kenwood-семантика: первый символ — пробел-
// заполнитель признака «не занято», далее сам текст; пустой аргумент = опрос
// занятости ('1' — очередь занята, логгер ждёт).
var Txt: string;
begin
if Length(s) = 0 then
begin
if Assigned(FCtx.CWTextBusy) and FCtx.CWTextBusy then Result := '1'
else Result := '0';
Exit;
end;
// Поле текста фиксированной ширины: 25 символов, добито пробелами справа.
// Длиннее — ошибка формата: очередь передачи не должна расти произвольно,
// иначе один пакет уводит станцию в эфир на неопределённое время.
if Length(s) > CW_TEXT_FIELD then Exit(CAT_ERROR);
Txt := s;
if (Length(Txt) > 0) and (Txt[1] = ' ') then Delete(Txt, 1, 1);
Txt := TrimRight(Txt); // иначе хвост набивки ушёл бы в эфир паузами
if Txt = '' then Exit('');
if Assigned(FCtx.SendCWText) then FCtx.SendCWText(Txt);
Result := '';
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: усиление микрофона по-кенвудовски (000..100). Шкала 100:70 к дБ — как в
// Thetis (n/1.43), дальше кламп под диапазон вкладки Transmit. Отрицательные дБ
// в кенвудовскую шкалу не ложатся: на чтении подрезаем к нулю.
var v: Integer;
begin
if Length(s) = 0 then
Result := Pad(EnsureRange(Round(SafeGetMicGain / 0.7), 0, 100), 3)
else if Length(s) = 3 then
begin
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
if Assigned(FCtx.SetMicGain) then
FCtx.SetMicGain(EnsureRange(Round(EnsureRange(v, 0, 100) / 1.43),
MIC_GAIN_MIN, MIC_GAIN_MAX));
Result := '';
end
else
Result := CAT_ERROR;
end;
function TCATEngine.CmdMO(const s: string): string;
// MO: монитор передачи — то же, что ZZMO.
begin
Result := ZZMO(s);
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: сдвиг репитера — обёртка над ZZOT, как и в эталоне.
begin
Result := ZZOT(s);
end;
function TCATEngine.CmdOS(const s: string): string;
// OS: направление сдвига репитера — обёртка над ZZOS, как и в эталоне.
begin
Result := ZZOS(s);
end;
function TCATEngine.ZZOS(const s: string): string;
// ZZOS: направление сдвига репитера. Снаружи (и у Thetis) 0=симплекс, 1=вверх,
// 2=вниз; внутри у нас RPT_NONE/MINUS/PLUS — отсюда трансляция.
// ★Мусор эталон молча трактует как симплекс (default в String2OffsetDirection),
// и мы повторяем это намеренно: клиенты рассчитывают на такое поведение.
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.ZZOT(const s: string): string;
// ZZOT: величина сдвига репитера, 9 цифр. Эталон читает поле как МГц с шестью
// знаками после запятой (вставляет точку после третьего разряда) — численно это
// ровно те же герцы, что и у нас. Знак не передаётся, направление — в ZZOS.
var v: Int64;
begin
if Length(s) = 9 then begin
// Нечисловое поле — ошибка формата: StrToIntDef молча обнулил бы сдвиг.
if not TryStrToInt64(s, v) then Exit(CAT_ERROR);
if Assigned(FCtx.SetFMRptOffset) then FCtx.SetFMRptOffset(v);
Result := '';
end else if Length(s) = 0 then
Result := Pad(Round(SafeGetFMRptOffset), 9)
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: речевой компрессор вкл/выкл. У Thetis команда только читает (всегда '0');
// у нас компрессор настоящий, поэтому SET тоже работает — расширение вверх,
// кенвудовские клиенты от него не ломаются.
begin
Result := OnOff(s, FCtx.GetCompOn, FCtx.SetCompOn);
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: убавить RIT (аргумент — 5 цифр смещения) — STUB: расстройки приёма нет.
// Шаг настройки вниз — это DN, перестройка — FA/FB.
begin
if (Length(s) = 0) or (Length(s) = 5) then Result := '' else Result := CAT_ERROR;
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: прибавить RIT — STUB, см. RD.
begin
if (Length(s) = 0) or (Length(s) = 5) then Result := '' else Result := CAT_ERROR;
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: верхняя кромка фильтра, индекс пресета (2 цифры). ⚠ У нас SH и SL ходят
// в ОДИН и тот же индекс пресета: раздельных «сторон» у главного приёмника нет.
// Кромки в герцах — это ZZFL/ZZFH.
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: нижняя кромка фильтра — тот же индекс пресета, что и SH (см. оговорку там).
begin
Result := CmdSH(s);
end;
function TCATEngine.CmdSM(const s: string): string;
// SM: S-метр. Запрос — один символ-селектор приёмника, ответ — ПЯТЬ цифр
// (шкала Kenwood 00000..00030: до 00015 это S0..S9, дальше +1 за каждый дБ).
// Селектор Thetis шлёт как '0' или '2', TS-2000 — как '0'/'1'; принимаем все:
// второго приёмника нет, отвечаем одним и тем же.
var sm: Integer;
begin
if (Length(s) = 1) and (s[1] in ['0', '1', '2']) then begin
sm := SMeterRaw(SafeGetSMeter);
sm := Round(sm * 30 / 260); // 0..260 (шкала ZZSM) → 0..30
Result := Pad(sm, 5);
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: сдвиг VFO A вниз на nn шагов — см. ZZAU (вверх)
else if ext = 'AE' then ans := '' // STUB: VFO A вниз на nn пресетных шагов — нет глобального шага, см. ZZAC
else if ext = 'AF' then ans := '' // STUB: VFO A вверх на nn пресетных шагов — см. ZZAE
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 — нет аудиопикового фильтра
else if ext = 'BA' then ans := ZZBA
else if ext = 'BB' then ans := '' // STUB: bandswitch RX2 вверх — RX2 нет
else if ext = 'BD' then begin ZZBD; ans := ''; end
else if ext = 'BE' then ans := ZZBE(s)
else if ext = 'BF' then ans := '' // STUB: VFO B вверх на nn пресетных шагов — см. ZZAE
else if ext = 'BG' then ans := '' // STUB: группа диапазонов HF/VHF — не разделяем
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: кнопка BCI Rejection — такого фильтра нет
else if ext = 'BS' then ans := ZZBS(s)
else if ext = 'BT' then ans := '' // STUB: диапазон RX2 — RX2 нет
else if ext = 'BU' then begin ZZBU; ans := ''; end
else if ext = 'BY' then ans := '' // STUB: выключение консоли — по CAT не гасимся
else if ext = 'CB' then ans := ZZCB(s)
else if ext = 'CD' then ans := ZZCD(s)
else if ext = 'CF' then ans := '' // STUB: показывать частоту телеграфа (UI Thetis)
else if ext = 'CI' then ans := ZZCI(s)
else if ext = 'CL' then ans := ZZCL(s)
else if ext = 'CM' then ans := ZZCM(s)
else if ext = 'CN' then ans := ZZCN(s)
else if ext = 'CO' then ans := '' // STUB: CTUN для RX2 — RX2 нет (CTUN RX1 — это ZZCN)
else if ext = 'CP' then ans := '' // STUB: компандер (у нас его нет — есть COMP, см. ZZPK)
else if ext = 'CS' then ans := ZZCS(s)
else if ext = 'CT' then ans := '' // STUB: порог компандера
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: усиление RX2 в окне Diversity — диверсити нет
else if ext = 'DD' then ans := '' // STUB: фаза в окне Diversity — диверсити нет
else if ext = 'DE' then ans := '' // STUB: кнопка Enable в окне Diversity — диверсити нет
else if ext = 'DF' then ans := '' // STUB: открыть/закрыть окно Diversity — диверсити нет
else if ext = 'DG' then ans := '' // STUB: усиление RX в окне Diversity — диверсити нет
else if ext = 'DH' then ans := '' // STUB: источник RX в окне Diversity — диверсити нет
else if ext = 'DM' then ans := ZZDM(s)
else if ext = 'DN' then ans := ZZDN(s)
else if ext = 'DO' then ans := '' // STUB: верхний уровень водопада — не проброшено (см. ZZDN)
else if ext = 'DP' then ans := '' // STUB: верх сетки спектра — не проброшено
else if ext = 'DQ' then ans := '' // STUB: низ сетки спектра — не проброшено
else if ext = 'DR' then ans := '' // STUB: шаг сетки спектра — не проброшено
else if ext = 'DU' then begin ZZDU; ans := ''; end
else if ext = 'DX' then ans := '' // STUB: кнопка DX (споты кластера) — флаг живёт в UI, не в контроллере
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: кнопка RXEQ — эквалайзер приёма
else if ext = 'ET' then ans := ZZET(s)
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 — RX2 нет
else if ext = 'FL' then ans := ZZFL(s)
else if ext = 'FM' then ans := ZZFM
else if ext = 'FR' then ans := '' // STUB: верхняя кромка фильтра RX2 — RX2 нет
else if ext = 'FS' then ans := '' // STUB: нижняя кромка фильтра RX2 — RX2 нет
else if ext = 'FT' then ans := ZZFT(s)
else if ext = 'FV' then ans := '' // STUB: чтение FlexWire (1 байт) — шины нет
else if ext = 'FW' then ans := '' // STUB: чтение FlexWire (2 байта) — шины нет
else if ext = 'FX' then ans := '' // STUB: запись FlexWire (1 байт) — шины нет
else if ext = 'FY' then ans := '' // STUB: запись FlexWire (2 байта) — шины нет
else if ext = 'GA' then ans := ZZGA(s)
else if ext = 'GE' then ans := '' // STUB: кнопка noise gate — такого блока нет
else if ext = 'GL' then ans := '' // STUB: уровень noise gate — такого блока нет
else if ext = 'GR' then ans := ZZGR(s)
else if ext = 'GT' then ans := ZZGT(s)
else if ext = 'GU' then ans := '' // STUB: постоянная AGC для RX2 — RX2 нет
else if ext = 'HA' then ans := '' // STUB: размер аудиобуфера — по CAT не крутим
else if ext = 'HR' then ans := '' // STUB: размер буфера DSP RX (телефония) — по CAT не крутим
else if ext = 'HT' then ans := '' // STUB: размер буфера DSP TX (телефония) — по CAT не крутим
else if ext = 'HU' then ans := '' // STUB: размер буфера DSP RX (CW) — по CAT не крутим
else if ext = 'HV' then ans := '' // STUB: размер буфера DSP TX (CW) — по CAT не крутим
else if ext = 'HW' then ans := '' // STUB: размер буфера DSP RX (digital) — по CAT не крутим
else if ext = 'HX' then ans := '' // STUB: размер буфера DSP TX (digital) — по CAT не крутим
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: сдвиг IF — полосу двигаем кромками (ZZFL/ZZFH)
else if ext = 'IU' then ans := '' // STUB: сброс сдвига фильтра — см. ZZIT
else if ext = 'JO' then ans := '' // STUB: нет такой команды у Thetis
else if ext = 'JP' then ans := '' // STUB: служебная JSON-команда Thetis
else if ext = 'JQ' then ans := '' // STUB: служебная JSON-команда Thetis
else if ext = 'JR' then ans := '' // STUB: служебная JSON-команда Thetis
else if ext = 'JS' then ans := '' // STUB: служебная JSON-команда Thetis
else if ext = 'KM' then ans := ZZKM(s)
else if ext = 'KO' then ans := '' // STUB: без описания у Thetis
else if ext = 'KS' then ans := ZZKS(s)
else if ext = 'KY' then ans := ZZKY(s)
else if ext = 'LA' then ans := '' // STUB: уровень RX0 — микшер Thetis
else if ext = 'LB' then ans := '' // STUB: стереобаланс главного приёмника — микшера нет
else if ext = 'LC' then ans := '' // STUB: уровень sub-RX — sub-RX нет
else if ext = 'LD' then ans := '' // STUB: стереобаланс sub-RX — sub-RX нет
else if ext = 'LE' then ans := '' // STUB: уровень RX2 — RX2 нет
else if ext = 'LF' then ans := '' // STUB: стереобаланс RX2 — RX2 нет
else if ext = 'LG' then ans := '' // STUB: автомьют RX1 при TX на VFO B — такой автоматики нет
else if ext = 'LH' then ans := '' // STUB: автомьют RX2 при TX на VFO A — RX2 нет
else if ext = 'LI' then ans := ZZLI(s)
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: без описания у Thetis
else if ext = 'MF' then ans := '' // STUB: текст на LCD мультифункционального энкодера Andromeda — не реализовано
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: режим измерителя приёма — метр один
else if ext = 'MS' then ans := '' // STUB: галка MultiRX Swap — MultiRX нет
else if ext = 'MT' then ans := '' // STUB: режим измерителя передачи — метр один
else if ext = 'MU' then ans := '' // STUB: кнопка MultiRX — MultiRX нет
else if ext = 'MV' then ans := ZZMV
else if ext = 'MW' then ans := '' // STUB: удалить канал памяти — память в CAT не проброшена
else if ext = 'MX' then ans := '' // STUB: восстановить канал памяти n — см. ZZMW
else if ext = 'MY' then ans := ZZMY
else if ext = 'MZ' then ans := '' // STUB: записать конфигурацию в канал памяти n — см. ZZMW
else if ext = 'NA' then ans := ZZNA(s)
else if ext = 'NB' then ans := ZZNB(s)
else if ext = 'NC' then ans := '' // STUB: NB для RX2 — RX2 нет
else if ext = 'ND' then ans := '' // STUB: NB2 для RX2 — RX2 нет
else if ext = 'NE' then ans := '' // STUB: статус NR RX1 (0..4) — дублирует ZZNR, не проброшено
else if ext = 'NF' then ans := '' // STUB: статус NR для RX2 — RX2 нет
else if ext = 'NG' then ans := '' // STUB: глубина подавления NR4 — такого алгоритма нет
else if ext = 'NH' then ans := '' // STUB: глубина подавления NR4 (вторая) — см. ZZNG
else if ext = 'NL' then ans := ZZNL(s)
else if ext = 'NM' then ans := '' // STUB: порог NB2 — порог бланкера не вынесен в настройки
else if ext = 'NN' then ans := ZZNN(s)
else if ext = 'NO' then ans := '' // STUB: спектральный бланкер RX2 — RX2 нет (RX1 — это ZZNN)
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: ANF для RX2 — RX2 нет
else if ext = 'NV' then ans := '' // STUB: статус шумоподавителя — дублирует ZZNR
else if ext = 'NW' then ans := '' // STUB: кнопка NR2 для RX2 — RX2 нет
else if ext = 'OA' then ans := ZZOA(s)
else if ext = 'OB' then ans := ZZOB(s)
else if ext = 'OC' then ans := '' // STUB: антенна передачи — порты AD936x в CAT не проброшены
else if ext = 'OD' then ans := '' // STUB: режим антенн — см. ZZOC
else if ext = 'OE' then ans := '' // STUB: галка внешней антенны RX1 — см. ZZOC
else if ext = 'OF' then ans := '' // STUB: реле TX на разъёме RCA — управления реле нет
else if ext = 'OG' then ans := '' // STUB: разрешение задержек реле TX — см. ZZOF
else if ext = 'OH' then ans := '' // STUB: величины задержек реле TX — см. ZZOF
else if ext = 'OJ' then ans := '' // STUB: без описания у Thetis
else if ext = 'OL' then ans := '' // STUB: click-tune офсет для DIGL — офсетов режима нет
else if ext = 'OS' then ans := ZZOS(s)
else if ext = 'OT' then ans := ZZOT(s)
else if ext = 'OU' then ans := '' // STUB: click-tune офсет для DIGU — см. ZZOL
else if ext = 'OV' then ans := '' // STUB: кнопка ATU — тюнера нет
else if ext = 'OW' then ans := '' // STUB: кнопка ATU Bypass — тюнера нет
else if ext = 'OX' then ans := '' // STUB: состояние настройки ATU Aries — тюнера нет
else if ext = 'OZ' then ans := '' // STUB: сброс состояния ATU Aries — тюнера нет
else if ext = 'PA' then ans := '' // STUB: преамп/аттенюатор — не проброшено (аттенюатор RX1 — ZZRX)
else if ext = 'PB' then ans := '' // STUB: преамп RX2 — RX2 нет
else if ext = 'PC' then ans := ZZPC(s)
else if ext = 'PD' then ans := ZZPD
else if ext = 'PE' then ans := '' // STUB: пан дисплея — не проброшено
else if ext = 'PK' then ans := ZZPK(s)
else if ext = 'PL' then ans := ZZPL(s)
else if ext = 'PO' then ans := ZZPO(s)
else if ext = 'PS' then ans := ZZPS(s)
else if ext = 'PY' then ans := '' // STUB: зум дисплея — не проброшено
else if ext = 'PZ' then ans := '' // STUB: кнопки зума дисплея — не проброшено
// ZZQK: у Thetis break-in делится на semi и QSK; у нас break-in один
// (прошивочный), а разница semi/QSK выражается hang-time'ом — ZZCD.
else if ext = 'QK' then ans := '' // STUB: нет отдельного переключателя semi/QSK
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: галка RTTY-офсета для VFO B — RTTY-движка нет
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 — расстройки приёма нет
else if ext = 'RH' then ans := '' // STUB: частота офсета RTTY DIGH — RTTY-движка нет
else if ext = 'RL' then ans := ZZRL(s)
else if ext = 'RM' then ans := '' // STUB: показание измерителя приёма — читается через ZZSM
else if ext = 'RS' then ans := '' // STUB: кнопка RX2 — RX2 нет
else if ext = 'RT' then ans := '' // STUB: кнопка RIT — расстройки приёма нет
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: шаговый аттенюатор RX2 — RX2 нет (RX1 — это ZZRX)
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: шаг настройки вниз колесом — это ZZSA
else if ext = 'SF' then ans := '' // STUB: фильтр «центр+ширина» одной командой — не проброшено
else if ext = 'SG' then ans := '' // STUB: VFO B вниз на один шаг — не проброшено (по индексу — ZZBM)
else if ext = 'SH' then ans := '' // STUB: VFO B вверх на один шаг — не проброшено (по индексу — ZZBP)
else if ext = 'SM' then ans := ZZSM(s)
else if ext = 'SN' then ans := '' // STUB: серийный номер радио — не проброшено
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: без описания у Thetis
else if ext = 'ST' then ans := ZZST(s)
else if ext = 'SU' then begin ZZSU; ans := ''; end
else if ext = 'SV' then ans := '' // STUB: шумоподавитель RX2 — RX2 нет
else if ext = 'SW' then ans := '' // STUB: обмен кнопок TX VFO A/B — не проброшено (обмен VFO — ZZVS)
else if ext = 'SX' then ans := '' // STUB: порог шумоподавителя RX2 — RX2 нет
else if ext = 'SY' then ans := '' // STUB: кнопка VFO Sync — синхронизации VFO нет
else if ext = 'SZ' then ans := '' // STUB: подгон VFO под шаг настройки — см. ZZSY
else if ext = 'TA' then ans := ZZTA(s)
else if ext = 'TB' then ans := ZZTB(s)
else if ext = 'TF' then ans := '' // STUB: показывать полосу TX на спектре (UI Thetis)
else if ext = 'TH' then ans := ZZTH(s)
else if ext = 'TI' then ans := '' // STUB: запрет выхода мощности (внешние антенны/тюнер)
else if ext = 'TL' then ans := ZZTL(s)
else if ext = 'TM' then ans := ZZTM(s)
else if ext = 'TO' then ans := ZZTO(s)
else if ext = 'TP' then ans := ZZTP(s)
else if ext = 'TS' then ans := '' // STUB: датчик температуры Flex 5000
else if ext = 'TU' then ans := ZZTU(s)
else if ext = 'TV' then ans := '' // STUB: TX-частота при split с RX2
else if ext = 'TX' then ans := ZZTX(s)
else if ext = 'UA' then ans := '' // STUB: имена диапазонов трансвертера — не проброшено
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 := '' // STUB: усиление приёма в VAC — VAC нет
else if ext = 'VC' then ans := '' // STUB: усиление передачи в VAC — VAC нет
else if ext = 'VD' then ans := '' // STUB: частота дискретизации VAC — VAC нет
else if ext = 'VE' then ans := '' // STUB: кнопка VOX — VOX нет
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: галка I/Q в VAC — VAC нет
else if ext = 'VI' then ans := '' // STUB: входной кабель VAC — VAC нет
else if ext = 'VJ' then ans := '' // STUB: галка Direct I/Q Use RX2 — RX2 нет
else if ext = 'VK' then ans := '' // STUB: галка VAC2 Enable — VAC нет
else if ext = 'VL' then ans := '' // STUB: кнопка блокировки VFO — не проброшено (SetVfoLock есть)
else if ext = 'VM' then ans := '' // STUB: драйвер VAC — VAC нет
else if ext = 'VN' then ans := ZZVN
else if ext = 'VO' then ans := '' // STUB: выходной кабель VAC — VAC нет
else if ext = 'VP' then ans := '' // STUB: галка VAC1 IQ Calibrate — VAC нет
else if ext = 'VQ' then ans := '' // STUB: драйвер VAC2 — VAC нет
else if ext = 'VR' then ans := '' // STUB: входной кабель VAC2 — VAC нет
else if ext = 'VS' then ans := ZZVS(s)
else if ext = 'VT' then ans := '' // STUB: выходной кабель VAC2 — VAC нет
else if ext = 'VU' then ans := '' // STUB: частота дискретизации VAC2 — VAC нет
else if ext = 'VV' then ans := '' // STUB: галка VAC2 Stereo — VAC нет
else if ext = 'VW' then ans := '' // STUB: без описания у Thetis
else if ext = 'VX' then ans := '' // STUB: усиление передачи в VAC2 — VAC нет
else if ext = 'VY' then ans := '' // STUB: размер буфера VAC1 — VAC нет
else if ext = 'VZ' then ans := '' // STUB: размер буфера VAC1 (второй) — VAC нет
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: уровень микрофона в микшере F1500 — железо другого вендора
else if ext = 'WU' then ans := '' // STUB: уровень входа FireWire в микшере F1500 — см. ZZWT
else if ext = 'WV' then ans := '' // STUB: уровень наушников в микшере F1500 — см. ZZWT
else if ext = 'WW' then ans := '' // STUB: уровень выхода FlexWire в микшере F1500 — см. ZZWT
else if ext = 'XA' then ans := ZZXA(s)
else if ext = 'XC' then ans := ZZXC
else if ext = 'XD' then ans := '' // STUB: убавить XIT — расстройки передачи нет
else if ext = 'XF' then ans := ZZXF(s)
else if ext = 'XH' then ans := '' // STUB: задержка VOX — VOX нет
else if ext = 'XN' then ans := '' // STUB: составной статус RX1 — не собираем
else if ext = 'XO' then ans := '' // STUB: составной статус RX2 — RX2 нет
else if ext = 'XS' then ans := '' // STUB: кнопка XIT — расстройки передачи нет
else if ext = 'XT' then ans := '' // STUB: кнопка X2TR — такого режима нет
else if ext = 'XU' then ans := '' // STUB: прибавить XIT — расстройки передачи нет
else if ext = 'XV' then ans := '' // STUB: составной статус VFO — не собираем
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: быстрый split вкл/выкл — не проброшено
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: усиление APF — STUB: аудиопикового фильтра в EWSDR нет.
// (Громкость — это ZZAG; раньше ZZAA был ошибочно заалиашен на неё.)
begin
if Length(s) = 4 then Result := '' else if Length(s) = 0 then Result := '0000'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZAB(const s: string): string;
// ZZAB: полоса APF — STUB: аудиопикового фильтра нет.
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: размер шага настройки консоли (индекс) — STUB: у EWSDR глобального
// шага нет, колесо/UP/DN ходят фиксированным шагом.
// (Выбор диапазона — это ZZBS; раньше он был заалиашен сюда.)
begin
if Length(s) = 2 then Result := '' else if Length(s) = 0 then Result := '00'
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: кнопка APF — STUB: аудиопикового фильтра нет.
// (Мощность — это ZZPC; раньше ZZAP был ошибочно заалиашен на неё.)
begin
Result := OnOff(s, nil, nil);
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).
// ★Нечисловое поле — ошибка формата: StrToIntDef молча дал бы индекс 0, то
// есть «ZZAUxx;» двигал бы VFO вместо честного «?;» (как в ZZFL/ZZFH).
var idx: Integer;
begin
if Length(s) = 2 then begin
if not TryStrToInt(s, idx) then Exit(CAT_ERROR);
idx := EnsureRange(idx, 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).
// Разбор поля — как у ZZAU: нечисловое значит ошибку, а не нулевой индекс.
var idx: Integer;
begin
if Length(s) = 2 then begin
if not TryStrToInt(s, idx) then Exit(CAT_ERROR);
idx := EnsureRange(idx, 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: AGC-T второго приёмника — STUB: 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: настройка APF — STUB: аудиопикового фильтра нет.
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: bandswitch второго приёмника вниз — STUB: 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: VFO B вниз на nn пресетных шагов — STUB: глобального размера шага у
// EWSDR нет (см. ZZAC). Шаг по индексу — это ZZBM/ZZBP.
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: кнопка BIN (бинауральный приём) — STUB: такого режима нет.
// (Индекс фильтра — это ZZFI; раньше он жил здесь.)
begin
Result := OnOff(s, nil, nil);
end;
function TCATEngine.ZZBM(const s: string): string;
// ZZBM: сдвиг VFO B ВНИЗ на один шаг с индексом nn (00..14) — пара к ZZBP.
// (Режим — это ZZMD; раньше ZZBM был ошибочно заалиашен на него.)
// Разбор поля — как у ZZAU: нечисловое значит ошибку, а не нулевой индекс.
var idx: Integer;
begin
if Length(s) = 2 then begin
if not TryStrToInt(s, idx) then Exit(CAT_ERROR);
idx := EnsureRange(idx, 0, 14);
if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(SafeGetVfoB - StepIdxToHz(idx));
Result := '';
end else
Result := CAT_ERROR;
end;
function TCATEngine.ZZBS(const s: string): string;
// ZZBS: выбор диапазона. ⚠ Отклонение от Thetis: там поле трёхсимвольное и
// несёт КОД диапазона ("160"/"040"/"WWV"), у нас — двузначный ИНДЕКС в списке
// диапазонов EWSDR. Менять поздно: на этом формате уже сидят клиенты.
// ★Нечисловое поле — ошибка формата: StrToIntDef переключал бы на диапазон 0,
// то есть уводил бы оператора с рабочего бэнда по опечатке клиента.
var idx: Integer;
begin
if Length(s) = 2 then begin
if not TryStrToInt(s, idx) then Exit(CAT_ERROR);
idx := EnsureRange(idx, 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.ZZBU: string;
begin
if Assigned(FCtx.DoBandUp) then FCtx.DoBandUp;
Result := '';
end;
function TCATEngine.ZZCB(const s: string): string;
// ZZCB: break-in — прошивка сама поднимает PTT на нажатие ключа.
begin
Result := OnOff(s, FCtx.GetCWBreakIn, FCtx.SetCWBreakIn);
end;
function TCATEngine.ZZCD(const s: string): string;
// ZZCD: задержка отпускания break-in (hang time), мс. Эталон принимает
// 150..5000, у нас потолок 2000 — это НАШ предел, а не описка: столько же
// стоит в настройках телеграфа и уходит в поле HangDelay пакета DUC Specific.
// Значение выше подрезается (как и всё прочее у Thetis), формат — те же 4 цифры.
begin
Result := NumField(s, 4, 0, CW_HANG_MAX, FCtx.GetCWHangTime, FCtx.SetCWHangTime);
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: скорость кейера, WPM (два разряда). Та же величина, что у KS/ZZKS —
// у Kenwood-команды поле шире, вот и всё различие.
begin
Result := NumField(s, 2, CW_WPM_MIN, CW_WPM_MAX, FCtx.GetCWSpeed, FCtx.SetCWSpeed);
end;
function TCATEngine.ZZDA(const s: string): string;
// ZZDA: усреднение спектра вкл/выкл — STUB: в CAT не проброшено.
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 — 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: режим дисплея (панадаптер/водопад/…) — STUB: в CAT не проброшено.
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: нижний уровень водопада, dBm — STUB: уровни водопада живут в per-pan
// конфиге, в контроллер пока не проброшены (см. §3 doc/CAT_STATUS.md).
// (Мощность — это ZZPC; раньше ZZDN был ошибочно заалиашен на неё.)
begin
if Length(s) = 4 then Result := '' else if Length(s) = 0 then Result := '-120'
else Result := CAT_ERROR;
end;
function TCATEngine.ZZDU: string;
// ZZDU: составное слово состояния для DDUtil — 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: значения эквалайзера ПРИЁМА — STUB: RX-эквалайзера нет (TX — ZZEB).
begin
if Length(s) > 0 then Result := '' else Result := ''; // no EQ
end;
function TCATEngine.ZZEB(const s: string): string;
// ZZEB: значения TX-эквалайзера одной строкой (формат Thetis).
// nnn + (nnn){nb+1}, добито нулями до 36 символов;
// nb — число полос, следом преамп и полосы, отрицательные как '-nn'.
var
G: TCATEQGains;
nb, i, v: Integer;
ans, fld: string;
begin
if Length(s) = 0 then
begin
FillChar(G, SizeOf(G), 0);
if Assigned(FCtx.GetTXEQ) then nb := FCtx.GetTXEQ(G) else nb := 0;
nb := EnsureRange(nb, 0, High(G));
ans := Pad(nb, 3);
for i := 0 to nb do
begin
v := EnsureRange(G[i], EQ_GAIN_MIN, EQ_GAIN_MAX);
if v < 0 then ans := ans + '-' + Pad(-v, 2)
else ans := ans + Pad(v, 3);
end;
while Length(ans) < 36 do ans := ans + '0';
Exit(Copy(ans, 1, 36));
end;
if Length(s) <> 36 then Exit(CAT_ERROR);
if not TryStrToInt(Copy(s, 1, 3), nb) then Exit(CAT_ERROR);
// Сетка эквалайзера бывает только трёх- или десятиполосной: движок
// (PushTXEQProfile) и редактор в настройках знают ровно эти два случая,
// любое другое число полос уехало бы в сохранённый TX-профиль мусором.
if (nb <> 3) and (nb <> 10) then Exit(CAT_ERROR);
FillChar(G, SizeOf(G), 0);
for i := 0 to nb do
begin
fld := Copy(s, 4 + i * 3, 3);
if not TryStrToInt(fld, v) then Exit(CAT_ERROR);
G[i] := EnsureRange(v, EQ_GAIN_MIN, EQ_GAIN_MAX);
end;
if Assigned(FCtx.SetTXEQ) then FCtx.SetTXEQ(nb, G);
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: девиация передачи — радиокнопка «узкая/широкая» (2.5/5 кГц). Поле в
// профиле непрерывное, поэтому на чтении делим по середине: всё, что ниже
// 5 кГц, отвечает узкой.
begin
if Length(s) = 0 then
Exit(B2C(SafeGetFMDeviation >= FM_DEV_WIDE));
if (s <> '0') and (s <> '1') then Exit(CAT_ERROR);
if Assigned(FCtx.SetFMDeviation) then
begin
if s = '1' then FCtx.SetFMDeviation(FM_DEV_WIDE)
else FCtx.SetFMDeviation(FM_DEV_NARROW);
end;
Result := '';
end;
function TCATEngine.ZZFI(const s: string): string;
// ZZFI: индекс текущего фильтра (2 цифры).
// Разбор поля — как у ZZFL/ZZFH: нечисловое значит ошибку, а не фильтр 0.
var idx: Integer;
begin
if Length(s) = 2 then begin
if not TryStrToInt(s, idx) then Exit(CAT_ERROR);
idx := EnsureRange(idx, 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.ZZFL(const s: string): string;
// ZZFL: нижняя кромка фильтра, Гц со знаком — поле фиксированное, ровно пять
// символов («-0150»). Нечисловой ввод раньше проваливался через StrToIntDef в
// ноль и МОЛЧА схлопывал кромку; теперь это ошибка формата.
var v: Integer;
begin
if Length(s) = 5 then begin
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
if Assigned(FCtx.SetFilterLow) then FCtx.SetFilterLow(v);
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: верхняя кромка фильтра, Гц со знаком — формат и разбор как у ZZFL.
var v: Integer;
begin
if Length(s) = 5 then begin
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
if Assigned(FCtx.SetFilterHigh) then FCtx.SetFilterHigh(v);
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: частота передачи. Со split'ом передача идёт с VFO B — туда и адресуем,
// иначе на VFO A. (Раньше команда всегда била в VFO A: split ещё не было.)
begin
if SafeGetSplit then Result := ZZFB(s) else Result := ZZFA(s);
end;
function TCATEngine.ZZGA(const s: string): string;
// ZZGA: регистрация GUID клиента — адресацией ответов ведает слой TCP.
// 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 клиента — см. ZZGA.
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.
begin
if Length(s) = 0 then Result := CmdIF
else Result := CAT_ERROR;
end;
function TCATEngine.ZZIO: string;
// ZZIO: список установленных опций — STUB: опций не заводим.
begin Result := '0'; end;
function TCATEngine.ZZIS(const s: string): string;
// ZZIS: ширина IF — STUB: полоса задаётся кромками (ZZFL/ZZFH) и пресетами.
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-макрос номер n (1..9) — у нас это ячейка памяти сообщений
// (окно CW Messages, F1..F8). Write-only, как в Thetis.
// Раньше здесь сидел режим кейера — своей команды у него в протоколе нет,
// иамбик вкл/выкл доступен через ZZCI.
var v: Integer;
begin
if Length(s) <> 1 then Exit(CAT_ERROR);
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
if (v < 1) or (v > 9) then Exit(CAT_ERROR);
if Assigned(FCtx.SendCWMacro) then FCtx.SendCWMacro(v);
Result := '';
end;
function TCATEngine.ZZKS(const s: string): string;
// ZZKS: скорость кейера, WPM.
begin
Result := CmdKS(s);
end;
function TCATEngine.ZZKY(const s: string): string;
// ZZKY: передать текст телеграфом (то же, что KY).
begin
Result := CmdKY(s);
end;
function TCATEngine.ZZMA(const s: string): string;
// ZZMA: кнопка MUT (заглушить приём).
// (Режим — это ZZMD; раньше ZZMA был ошибочно заалиашен на него.)
begin
Result := OnOff(s, FCtx.GetMuted, FCtx.SetMuted);
end;
function TCATEngine.ZZMB(const s: string): string;
// ZZMB: кнопка MUT второго приёмника — STUB: RX2 нет.
begin
Result := OnOff(s, nil, nil);
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: усиление микрофона в дБ. Формат Thetis: SET — три цифры либо знак+две
// ('+10'/'-05'), GET всегда отдаёт знак+две. Значение уезжает в активный
// TX-профиль (MicGainDB), поэтому подрезаем под диапазон вкладки Transmit.
var v: Integer;
begin
if Length(s) = 0 then
Exit(SignedPad(EnsureRange(SafeGetMicGain, MIC_GAIN_MIN, MIC_GAIN_MAX), 2));
// Три символа (`025`, `+10`, `-05`) либо четыре со знаком (`-050`) — ровно
// как у Thetis (nSet и nSet+1). Более короткие поля — ошибка формата, а не
// «маленькое значение»: молча крутить усиление по ним нельзя.
if (Length(s) < 3) or (Length(s) > 4) then Exit(CAT_ERROR);
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
if Assigned(FCtx.SetMicGain) then
FCtx.SetMicGain(EnsureRange(v, MIC_GAIN_MIN, MIC_GAIN_MAX));
Result := '';
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: имя режима по номеру. ⚠ Отклонение от Thetis: там ZZMN отдаёт пресеты
// DSP-фильтров (180 символов). Имён режимов в протоколе нет вовсе, а список
// режимов (ZZML) на эту команду опирается — оставляем как расширение EWSDR.
// Индекс режима двузначный (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: кнопка MON. У EWSDR монитор передачи — это self-monitor даунлинка
// (RX MUTE снят); слышно его, разумеется, только в дуплексе. Уровень — ZZTM.
begin
Result := OnOff(s, FCtx.GetMonitorOn, FCtx.SetMonitorOn);
end;
function TCATEngine.ZZMV: string;
// ZZMV: число записей в памяти каналов (read-only).
// (Текущий режим — это ZZMD; раньше ZZMV отдавал его индекс.)
var n: Integer;
begin
if Assigned(FCtx.GetMemoryCount) then n := FCtx.GetMemoryCount else n := 0;
Result := Pad(n, 3);
end;
function TCATEngine.ZZMY: string;
// ZZMY: mode Y — STUB
begin Result := ''; end;
function TCATEngine.ZZNA(const s: string): string;
// ZZNA: кнопка NB1 (у нас режимы NB нумерованы, 1 = первый бланкер).
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: порог NB1 — STUB: порог бланкера не вынесен в настройки.
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: спектральный бланкер (SNB) RX1.
// (Раньше здесь стояла заглушка «ручной нотч», а SNB сидел в ZZNS.)
begin
Result := OnOff(s, FCtx.GetSNBEnabled, FCtx.SetSNBEnabled);
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: кнопка NR2 — у нас шумоподавитель один и режимный, NR2 = режим 2.
// Включение ставит режим 2, выключение — 0; прочие режимы читаются как '0'.
// (SNB переехал в ZZNN, где ему и место по протоколу.)
const NR2 = 2;
begin
if Length(s) = 0 then Exit(B2C(SafeGetNRMode = NR2));
if (s <> '0') and (s <> '1') then Exit(CAT_ERROR);
if Assigned(FCtx.SetNRMode) then
begin
if s = '1' then FCtx.SetNRMode(NR2)
else if SafeGetNRMode = NR2 then FCtx.SetNRMode(0);
end;
Result := '';
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 — STUB: порты AD936x в контроллере есть (SetAnt936x), но в
// CAT не проброшены (см. §3 doc/CAT_STATUS.md).
// (Громкость — это ZZAG; раньше ZZOA был ошибочно заалиашен на неё.)
begin
Result := OnOff(s, nil, nil);
end;
function TCATEngine.ZZOB(const s: string): string;
// ZZOB: антенна RX2 — STUB: 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: центрировать пан дисплея — STUB: пан/зум в CAT не проброшены.
// (Раньше команда отдавала «номинальную мощность 100» — выдумка.)
begin Result := ''; end;
function TCATEngine.ZZPO(const s: string): string;
// ZZPO: кнопка Peak на дисплее — STUB: удержания пика в CAT нет.
// (Питание/состояние — это ZZPS; раньше ZZPO отвечал за него.)
begin
Result := OnOff(s, nil, nil);
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: частота из быстрой памяти (11 цифр) — STUB: быстрая память не заведена.
// (Текущий режим — это ZZMD; раньше ZZQM отдавал его.)
begin Result := Pad(0, 11); 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: галка RTTY-офсета для VFO A — STUB: RTTY-движка нет.
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: убавить RIT — STUB: расстройки приёма в EWSDR нет.
// (Шаг настройки вниз — это ZZSA/ZZSD и кенвудовская RD.)
begin
if (Length(s) = 0) or (Length(s) = 5) then Result := '' else Result := CAT_ERROR;
end;
function TCATEngine.ZZRL(const s: string): string;
// ZZRL: частота офсета RTTY DIGL — STUB: RTTY-движка нет.
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: прибавить RIT — STUB: расстройки приёма в EWSDR нет.
// (Шаг настройки вверх — это ZZSB/ZZSU и кенвудовская RU.)
begin
if (Length(s) = 0) or (Length(s) = 5) then Result := '' else Result := CAT_ERROR;
end;
function TCATEngine.ZZRV: string;
// ZZRV: напряжение питания PA, формат Thetis «NN.N» (4 символа).
// (Версия ПО — это ZZVN; раньше ZZRV отдавал её.)
var v: Double;
begin
if Assigned(FCtx.GetSupplyVolts) then v := FCtx.GetSupplyVolts else v := -1;
if v < 0 then v := 0; // телеметрии нет — отвечаем нулём, как Thetis
if v > 99.9 then v := 99.9;
Result := Format('%04.1f', [v]);
end;
function TCATEngine.ZZRX(const s: string): string;
// ZZRX: шаговый аттенюатор RX1, дБ (00..31).
// (Переход на приём — это ZZTX0 и кенвудовская RX; раньше ZZRX делал его.)
begin
Result := NumField(s, 2, 0, 31, FCtx.GetAtten, FCtx.SetAtten);
end;
function TCATEngine.ZZSA: string;
// ZZSA: шаг настройки вниз. У Thetis шагает именно VFO A, у нас — активный VFO
// (отдельного «шага VFO A» в контроллере нет).
begin
if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown;
Result := '';
end;
function TCATEngine.ZZSB: string;
// ZZSB: шаг настройки вверх — см. оговорку в ZZSA.
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: кнопка Spur Reduction — STUB: такого блока в тракте нет.
// (Опорный уровень спектра — это ZZDP; раньше ZZSR отвечал за него.)
begin
Result := OnOff(s, nil, nil);
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 вкл/выкл для FM-передачи. Значение строго 0/1 — иначе «CT9;»
// молча выключал бы тон вместо ответа об ошибке.
begin
Result := OnOff(s, FCtx.GetCTCSSOn, FCtx.SetCTCSSOn);
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: кнопка MOX — только 0 (приём) и 1 (передача). Никакого «2=TUNE» в
// протоколе нет (настройка — это ZZTU): на любом другом значении отвечаем
// ошибкой и НЕ поднимаем передачу.
begin
Result := OnOff(s, FCtx.GetTransmitting, FCtx.SetTransmitting);
end;
function TCATEngine.ZZUP(const s: string): string;
// ZZUP: внешний PA вкл/выкл — STUB: управления внешним усилителем нет.
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: запустить одиночную калибровку PureSignal (write-only).
begin
if Assigned(FCtx.DoPSSingleCal) then FCtx.DoPSSingleCal;
Result := '';
end;
function TCATEngine.ZZUT(const s: string): string;
// ZZUT: двухтональник (2TON).
begin
Result := OnOff(s, FCtx.GetTwoTone, FCtx.SetTwoTone);
end;
function TCATEngine.ZZUX(const s: string): string;
// ZZUX: блокировка VFO A — STUB: у нас лок общий (SetVfoLock), раздельного нет.
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 — STUB: см. ZZUX.
begin Result := ZZUX(s); end;
function TCATEngine.ZZVA(const s: string): string;
// ZZVA: включение VAC — STUB: виртуального аудиокабеля в EWSDR нет.
// (Обмен VFO — это ZZVS; раньше он жил здесь.)
begin
Result := OnOff(s, nil, nil);
end;
function TCATEngine.ZZVG(const s: string): string;
// ZZVG: усиление VOX — STUB: VOX'а нет.
// (Громкость — это ZZAG; раньше ZZVG был ошибочно заалиашен на неё.)
begin
if Length(s) = 4 then Result := '' else if Length(s) = 0 then Result := '0000'
else Result := CAT_ERROR;
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 (write-only). Аргумент — НОМЕР операции, а не
// флаг: 0 = A→B, 1 = B→A, 2 = обмен, 3 = IF→VFO.
// (Split — это ZZSP; раньше ZZVS был заалиашен на него.)
begin
if Length(s) <> 1 then Exit(CAT_ERROR);
case s[1] of
'0': if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(SafeGetVfoA);
'1': if Assigned(FCtx.SetVfoA) then FCtx.SetVfoA(SafeGetVfoB);
'2': if Assigned(FCtx.DoVfoSwap) then FCtx.DoVfoSwap;
// IF→VFO переносит расстройку приёма в частоту VFO. Расстройки у нас нет,
// переносить нечего — команду принимаем и ничего не делаем.
'3': ;
else
Exit(CAT_ERROR);
end;
Result := '';
end;
// ZZWA..ZZWS: микшер Flex 5000 / FLEX-1500 (уровни и галки входов/выходов) —
// STUB: железо другого вендора, аналога нет. Водопад тут ни при чём: его
// уровни живут в ZZDN/ZZDO.
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;
// ZZXA: включение аудиоусилителя G2; ZZXC/ZZXF: сброс и значение XIT —
// STUB: усилителя не касаемся, расстройки передачи (XIT) в EWSDR нет.
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;
// ZZYA/ZZYB: галки Direct I/Q и IQ Calibrate для VAC2 — STUB: VAC нет.
// ZZYC: отдельное усиление микрофона в FM — STUB: у нас оно одно на все режимы
// (ZZMG). ZZYR: переключатель RX1/RX2 — STUB: RX2 нет.
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;
// ZZZA: трип-состояние усилителя Ganymede; ZZZB, ZZZQ, ZZZR: внутренние
// служебные Thetis; ZZZM/ZZZV: строки модели и версии железа; ZZZT: зум на
// диапазон; ZZZW: обмен «колёс» VFO в midi2cat; ZZZO: быстрый split.
// Всё — STUB: либо чужое железо, либо в CAT не проброшено.
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: без описания у Thetis — STUB.
begin Result := ''; end;
function TCATEngine.ZZZO(const s: string): string;
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
{ ── TX-тракт и телеграф ──────────────────────────────────────────────────── }
function TCATEngine.ZZCI(const s: string): string;
// ZZCI: иамбический манипулятор вкл/выкл. У нас режим кейера трёхпозиционный
// (прямой ключ / iambic A / iambic B) — включение выбирает B, как в Thetis по
// умолчанию, а если иамбик уже стоит, ранее выбранная буква сохраняется.
const KEYER_STRAIGHT = 0; KEYER_IAMBIC_B = 2;
begin
if Length(s) = 0 then Exit(B2C(SafeGetCWKeyerMode <> KEYER_STRAIGHT));
if (s <> '0') and (s <> '1') then Exit(CAT_ERROR);
if Assigned(FCtx.SetCWKeyerMode) then
begin
if s = '0' then FCtx.SetCWKeyerMode(KEYER_STRAIGHT)
else if SafeGetCWKeyerMode = KEYER_STRAIGHT then FCtx.SetCWKeyerMode(KEYER_IAMBIC_B);
end;
Result := '';
end;
function TCATEngine.ZZCL(const s: string): string;
// ZZCL: тон телеграфа (он же сайдтон), Гц.
begin
Result := NumField(s, 4, CW_PITCH_MIN, CW_PITCH_MAX, FCtx.GetCWPitch, FCtx.SetCWPitch);
end;
function TCATEngine.ZZCM(const s: string): string;
// ZZCM: у Thetis это «CW Monitor DISABLE» — значение инвертировано к сайдтону.
begin
if Length(s) = 0 then Exit(B2C(not SafeGetBool(FCtx.GetCWMonitor)));
if (s <> '0') and (s <> '1') then Exit(CAT_ERROR);
if Assigned(FCtx.SetCWMonitor) then FCtx.SetCWMonitor(s = '0');
Result := '';
end;
function TCATEngine.ZZET(const s: string): string;
// ZZET: кнопка TXEQ.
begin
Result := OnOff(s, FCtx.GetTXEQOn, FCtx.SetTXEQOn);
end;
function TCATEngine.ZZLI(const s: string): string;
// ZZLI: кнопка PS-A (автокалибровка PureSignal).
begin
Result := OnOff(s, FCtx.GetPureSignal, FCtx.SetPureSignal);
end;
function TCATEngine.ZZPK(const s: string): string;
// ZZPK: речевой компрессор (WDSP COMP) вкл/выкл. Пара к ZZPL.
begin
Result := OnOff(s, FCtx.GetCompOn, FCtx.SetCompOn);
end;
function TCATEngine.ZZPL(const s: string): string;
// ZZPL: усиление речевого компрессора, дБ.
begin
Result := NumField(s, 2, 0, COMP_GAIN_MAX, FCtx.GetCompGain, FCtx.SetCompGain);
end;
function TCATEngine.ZZTH(const s: string): string;
// ZZTH: верхняя кромка TX-фильтра, Гц.
begin
Result := NumField(s, 5, TX_FILT_HI_MIN, TX_FILT_HI_MAX,
FCtx.GetTXFilterHigh, FCtx.SetTXFilterHigh);
end;
function TCATEngine.ZZTL(const s: string): string;
// ZZTL: нижняя кромка TX-фильтра, Гц.
begin
Result := NumField(s, 4, TX_FILT_LO_MIN, TX_FILT_LO_MAX,
FCtx.GetTXFilterLow, FCtx.SetTXFilterLow);
end;
function TCATEngine.ZZTM(const s: string): string;
// ZZTM: громкость монитора передачи, 0..100.
begin
Result := NumField(s, 3, 0, 100, FCtx.GetMonitorLevel, FCtx.SetMonitorLevel);
end;
function TCATEngine.ZZTO(const s: string): string;
// ZZTO: мощность настройки (уровень TUN), % от полной шкалы Drive.
begin
Result := NumField(s, 3, 0, 100, FCtx.GetTUNLevel, FCtx.SetTUNLevel);
end;
function TCATEngine.ZZTP(const s: string): string;
// ZZTP: активный TX-профиль по индексу. Индекса вне списка нет — это ошибка
// выполнения, а не подрезка: клиент выбирает из того, что ему отдал ZZTP;.
var v, Cnt: Integer;
begin
if Length(s) = 0 then Exit(Pad(Max(0, SafeGetTXProfile), 2));
if Length(s) <> 2 then Exit(CAT_ERROR);
if not TryStrToInt(s, v) then Exit(CAT_ERROR);
if Assigned(FCtx.GetTXProfileCount) then Cnt := FCtx.GetTXProfileCount else Cnt := 0;
if (v < 0) or (v >= Cnt) then Exit(CAT_ERROR);
if Assigned(FCtx.SetTXProfile) then FCtx.SetTXProfile(v);
Result := '';
end;
function TCATEngine.ZZTU(const s: string): string;
// ZZTU: кнопка TUN.
begin
Result := OnOff(s, FCtx.GetTuning, FCtx.SetTuning);
end;
end.