unit CATEngine; { CATEngine.pas — CAT command engine (Kenwood + Thetis ZZ* extensions). Совместимость с Thetis/PowerSDR: все команды из CATParser.cs + CATCommands.cs реализованы. Команды, для которых в ewsdr ещё нет функционала, возвращают заглушку и сопровождены комментарием "STUB: <причина>". Формат команд: SET: ; (suffix = значение) GET: ; (suffix пустой) Resp: ; Err: ?; (неверный формат) | E; (ошибка выполнения) Пример: "FA00014250000;" — set VFO A = 14.250 MHz "FA;" — get VFO A "ZZFA00014250000;" — то же расширенной командой } {$IFDEF FPC} {$MODE Delphi} {$ENDIF} interface uses Classes, SysUtils, Math, SyncObjs, 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 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; // Двухпозиционная команда «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 0–255 (Kenwood) ↔ 0–100 (ewsdr) var v: Integer; begin if Length(s) = 4 then begin if not TryStrToInt(Copy(s, 2, 3), v) then Exit(CAT_ERROR); v := EnsureRange(Round(v / 2.55), 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 if not TryStrToInt(s, idx) then Exit(CAT_ERROR); idx := EnsureRange(idx - 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 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, 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 var m: Integer; begin if Length(s) = 3 then begin if not TryStrToInt(s, m) then Exit(CAT_ERROR); if Assigned(FCtx.SetAGCMode) then FCtx.SetAGCMode(EnsureRange(m, 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 var v: Integer; begin if Length(s) = 1 then begin if not TryStrToInt(s, v) then Exit(CAT_ERROR); 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.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 if not TryStrToInt(s, v) then Exit(CAT_ERROR); v := EnsureRange(v, 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 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.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=0/1 RX, lll=000–255). GET: . // Шкала Kenwood 0–255 маппится на внутренний уровень 0–100 %. var lvl255, lvl100: Integer; begin if Length(s) = 4 then begin if not TryStrToInt(Copy(s, 2, 3), lvl255) then Exit(CAT_ERROR); lvl255 := EnsureRange(lvl255, 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 := '' // STUB: VFO B вниз на nn пресетных шагов — см. ZZAE 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 0–100 var v: Integer; begin if Length(s) = 3 then begin if not TryStrToInt(s, v) then Exit(CAT_ERROR); v := EnsureRange(v, 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 if not TryStrToInt(s, v) then Exit(CAT_ERROR); // Val понимает ведущий +/- 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.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; // Отдаём ровно то, что примем обратно (см. разбор SET ниже): сетка бывает // только трёх- или десятиполосной, а движок всё, что не 3, играет как 10. if nb <> 3 then nb := 10; 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: режим АРУ (0=FAST,1=MED,2=SLOW,3=LONG,4=OFF). Ответ — три цифры, как у // кенвудовской GT, и SET обязан принимать ту же ширину: клиент, прочитавший // «ZZGT000;», пишет назад ровно это, и односимвольная форма отдавала ему «?;». // Однобуквенную форму оставляем — на ней уже сидят клиенты. var m: Integer; begin if Length(s) = 0 then Exit(Pad(SafeGetAGCMode, 3)); if (Length(s) <> 1) and (Length(s) <> 3) then Exit(CAT_ERROR); if not TryStrToInt(s, m) then Exit(CAT_ERROR); m := EnsureRange(m, 0, 4); if Assigned(FCtx.SetAGCMode) then FCtx.SetAGCMode(m); Result := ''; 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 if not TryStrToInt(s, v) then Exit(CAT_ERROR); 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 if not TryStrToInt(s, v) then Exit(CAT_ERROR); v := EnsureRange(v, 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 if not TryStrToInt(s, v) then Exit(CAT_ERROR); v := EnsureRange(v, 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 0–100 → DriveLevel var v: Integer; begin if Length(s) = 3 then begin if not TryStrToInt(s, v) then Exit(CAT_ERROR); v := EnsureRange(v, 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, 000–100 %). Уровень>0 включает шумоподавитель, // 000 — открывает (выключает). Маппится на FM-squelch контроллера. var v: Integer; begin if Length(s) = 3 then begin if not TryStrToInt(s, v) then Exit(CAT_ERROR); v := EnsureRange(v, 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. var v: Integer; begin if Length(s) = 2 then begin if not TryStrToInt(s, v) then Exit(CAT_ERROR); if Assigned(FCtx.SetFMStep) then FCtx.SetFMStep(EnsureRange(v, 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). var v: Integer; begin if Length(s) = 2 then begin if not TryStrToInt(s, v) then Exit(CAT_ERROR); if Assigned(FCtx.SetCTCSSTone) then FCtx.SetCTCSSTone(EnsureRange(v, 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.