diff --git a/CATEngine.pas b/CATEngine.pas new file mode 100644 index 0000000..a19a97f --- /dev/null +++ b/CATEngine.pas @@ -0,0 +1,2062 @@ +unit CATEngine; +{ + CATEngine.pas — CAT command engine (Kenwood + Thetis ZZ* extensions). + + Совместимость с Thetis/PowerSDR: все команды из CATParser.cs + CATCommands.cs + реализованы. Команды, для которых в ewsdr ещё нет функционала, возвращают + заглушку и сопровождены комментарием "STUB: <причина>". + + Формат команд: + SET: ; (suffix = значение) + GET: ; (suffix пустой) + Resp: ; + Err: ?; (неверный формат) | E; (ошибка выполнения) + + Пример: + "FA00014250000;" — set VFO A = 14.250 MHz + "FA;" — get VFO A + "ZZFA00014250000;" — то же расширенной командой +} +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +interface + +uses + Classes, SysUtils, Math, SyncObjs; + +const + CAT_ERROR = '?;'; + CAT_EXEC_ERROR = 'E;'; + CAT_ID_NUM = '019'; // TS-2000 compatible + + // Mode mapping ewsdr → CAT + MODE_LSB = 0; MODE_USB = 1; MODE_DSB = 2; + MODE_CWL = 3; MODE_CWU = 4; MODE_FM = 5; + MODE_AM = 6; MODE_SAM = 7; + +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; + + 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; + end; + + TCATEngine = class + private + FCtx: TCATContext; + FLock: TCriticalSection; + + function Pad(V, W: Integer): string; + function FreqToStr(Hz: Double): string; + function StrToFreq(const S: string): Double; + function ModeToK(M: Integer): Integer; + function KToMode(K: Integer): Integer; + function SMeterRaw(dBm: Double): Integer; + + // helpers to safely call context callbacks + function SafeGetVfoA: Double; + function SafeGetVfoB: Double; + function SafeGetMode: Integer; + function SafeGetVolume: Integer; + function SafeGetDriveLevel: Integer; + function SafeGetFilterIdx: Integer; + function SafeGetAGCMode: Integer; + function SafeGetNRMode: Integer; + function SafeGetNBMode: Integer; + function SafeGetSNB: Boolean; + function SafeGetANF: Boolean; + function SafeGetTX: Boolean; + function SafeGetRunning: Boolean; + function SafeGetSMeter: Double; + function SafeGetBand: Integer; + + // 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 ZZBA: string; + function ZZBD: string; + function ZZBE(const s: string): string; + function ZZBI(const s: string): string; + function ZZBM(const s: string): string; + function ZZBS(const s: string): string; + function ZZBU: string; + function ZZCB(const s: string): string; + function ZZCD(const s: string): string; + function ZZCN(const s: string): string; + function ZZCS(const s: string): string; + function ZZDA(const s: string): string; + function ZZDB(const s: string): string; + function ZZDM(const s: string): string; + function ZZDN(const s: string): string; + function ZZDU: string; + function ZZEA(const s: string): string; + function ZZEB(const s: string): string; + function ZZEM(const s: string): string; + function ZZFA(const s: string): string; + function ZZFB(const s: string): string; + function ZZFD(const s: string): string; + function ZZFI(const s: string): string; + function ZZFL(const s: string): string; + function ZZFH(const s: string): string; + function ZZFM: string; + function ZZFT(const s: string): string; + function ZZGA(const s: string): string; + function ZZGR(const s: string): string; + function ZZGT(const s: string): string; + function ZZID: string; + function ZZIF(const s: string): string; + function ZZIO: string; + function ZZIS(const s: string): string; + function ZZKM(const s: string): string; + function ZZKS(const s: string): string; + function ZZKY(const s: string): string; + function ZZMA(const s: string): string; + function ZZMB(const s: string): string; + function ZZMD(const s: string): string; + function ZZMG(const s: string): string; + function ZZML: string; + function ZZMN(const s: string): string; + function ZZMO(const s: string): string; + function ZZMV: string; + function ZZMY: string; + function ZZNA(const s: string): string; + function ZZNB(const s: string): string; + function ZZNL(const s: string): string; + function ZZNN(const s: string): string; + function ZZNR(const s: string): string; + function ZZNS(const s: string): string; + function ZZNT(const s: string): string; + function ZZOA(const s: string): string; + function ZZOB(const s: string): string; + function ZZPC(const s: string): string; + function ZZPD: string; + function ZZPO(const s: string): string; + function ZZPS(const s: string): string; + function ZZQM: string; + function ZZQR: string; + function ZZQS: string; + function ZZRA(const s: string): string; + function ZZRC: string; + function ZZRD(const s: string): string; + function ZZRL(const s: string): string; + function ZZRU(const s: string): string; + function ZZRV: string; + function ZZRX(const s: string): string; + function ZZSA: string; + function ZZSB: string; + function ZZSM(const s: string): string; + function ZZSO(const s: string): string; + function ZZSP(const s: string): string; + function ZZSQ(const s: string): string; + function ZZSR(const s: string): string; + function ZZST: string; + function ZZSU: string; + function ZZTA(const s: string): string; + function ZZTB(const s: string): string; + function ZZTX(const s: string): string; + function ZZUP(const s: string): string; + function ZZUS: string; + function ZZUT(const s: string): string; + function ZZUX(const s: string): string; + function ZZUY(const s: string): string; + function ZZVA(const s: string): string; + function ZZVB(const s: string): string; + function ZZVG(const s: string): string; + function ZZVN: string; + function ZZVS(const s: string): string; + function ZZWA(const s: string): string; + function ZZWB(const s: string): string; + function ZZWC(const s: string): string; + function ZZWD(const s: string): string; + function ZZWE(const s: string): string; + function ZZWF(const s: string): string; + function ZZWG(const s: string): string; + function ZZWH(const s: string): string; + function ZZWJ(const s: string): string; + function ZZWK(const s: string): string; + function ZZWL(const s: string): string; + function ZZWM(const s: string): string; + function ZZWN(const s: string): string; + function ZZWO(const s: string): string; + function ZZWP(const s: string): string; + function ZZWQ(const s: string): string; + function ZZWR(const s: string): string; + function ZZWS(const s: string): string; + function ZZXC: string; + function ZZXF(const s: string): string; + function ZZXA(const s: string): string; + function ZZYA(const s: string): string; + function ZZYB(const s: string): string; + function ZZYC(const s: string): string; + function ZZYR(const s: string): string; + function ZZZA(const s: string): string; + function ZZZB: string; + function ZZZM(const s: string): string; + function ZZZP(const s: string): string; + function ZZZQ(const s: string): string; + function ZZZR(const s: string): string; + function ZZZS(const s: string): string; + function ZZZT(const s: string): string; + function ZZZU(const s: string): string; + function ZZZV(const s: string): string; + function ZZZW(const s: string): string; + function ZZZZ: string; + function ZZZO(const s: string): string; + + public + constructor Create(const Ctx: TCATContext); + destructor Destroy; override; + function Parse(const Cmd: string): string; + end; + +implementation + +{ ── Local helpers ────────────────────────────────────────────────────────── } + +// Convert Boolean to CAT '1'/'0' string — avoids IfThen char-overload ambiguity +function B2C(B: Boolean): string; inline; +begin if B then Result := '1' else Result := '0'; end; + +{ ── Helpers ──────────────────────────────────────────────────────────────── } + +function TCATEngine.Pad(V, W: Integer): string; +begin + Result := IntToStr(Abs(V)); + while Length(Result) < W do Result := '0' + Result; + if Length(Result) > W then Result := Copy(Result, Length(Result) - W + 1, W); +end; + +function TCATEngine.FreqToStr(Hz: Double): string; +begin + Result := Pad(Round(Hz), 11); +end; + +function TCATEngine.StrToFreq(const S: string): Double; +var V: Int64; +begin + if TryStrToInt64(S, V) then + Result := V + else + Result := 0; +end; + +function TCATEngine.ModeToK(M: Integer): Integer; +begin + case M of + MODE_LSB: Result := 1; + MODE_USB: Result := 2; + MODE_DSB: Result := 2; // treated as USB for CAT compatibility + MODE_CWU: Result := 3; + MODE_FM: Result := 4; + MODE_AM: Result := 5; + MODE_SAM: Result := 5; // treated as AM + MODE_CWL: Result := 7; + else Result := 2; + end; +end; + +function TCATEngine.KToMode(K: Integer): Integer; +begin + case K of + 1: Result := MODE_LSB; + 2: Result := MODE_USB; + 3: Result := MODE_CWU; + 4: Result := MODE_FM; + 5: Result := MODE_AM; + 6: Result := MODE_USB; // FSK/RTTY → USB + 7: Result := MODE_CWL; + 9: Result := MODE_LSB; // FSK-R → LSB + else Result := MODE_USB; + end; +end; + +function TCATEngine.SMeterRaw(dBm: Double): Integer; +var v: Double; +begin + v := (dBm + 140.0) * 2.0; + if v < 0 then v := 0; + if v > 260 then v := 260; + Result := Round(v); +end; + +{ ── Safe getters (handle nil callbacks gracefully) ───────────────────────── } + +function TCATEngine.SafeGetVfoA: Double; +begin + if Assigned(FCtx.GetVfoA) then Result := FCtx.GetVfoA else Result := 14200000; +end; +function TCATEngine.SafeGetVfoB: Double; +begin + if Assigned(FCtx.GetVfoB) then Result := FCtx.GetVfoB else Result := 14200000; +end; +function TCATEngine.SafeGetMode: Integer; +begin + if Assigned(FCtx.GetMode) then Result := FCtx.GetMode else Result := MODE_USB; +end; +function TCATEngine.SafeGetVolume: Integer; +begin + if Assigned(FCtx.GetVolume) then Result := FCtx.GetVolume else Result := 50; +end; +function TCATEngine.SafeGetDriveLevel: Integer; +begin + if Assigned(FCtx.GetDriveLevel) then Result := FCtx.GetDriveLevel else Result := 50; +end; +function TCATEngine.SafeGetFilterIdx: Integer; +begin + if Assigned(FCtx.GetFilterIdx) then Result := FCtx.GetFilterIdx else Result := 0; +end; +function TCATEngine.SafeGetAGCMode: Integer; +begin + if Assigned(FCtx.GetAGCMode) then Result := FCtx.GetAGCMode else Result := 0; +end; +function TCATEngine.SafeGetNRMode: Integer; +begin + if Assigned(FCtx.GetNRMode) then Result := FCtx.GetNRMode else Result := 0; +end; +function TCATEngine.SafeGetNBMode: Integer; +begin + if Assigned(FCtx.GetNBMode) then Result := FCtx.GetNBMode else Result := 0; +end; +function TCATEngine.SafeGetSNB: Boolean; +begin + if Assigned(FCtx.GetSNBEnabled) then Result := FCtx.GetSNBEnabled else Result := False; +end; +function TCATEngine.SafeGetANF: Boolean; +begin + if Assigned(FCtx.GetANFEnabled) then Result := FCtx.GetANFEnabled else Result := False; +end; +function TCATEngine.SafeGetTX: Boolean; +begin + if Assigned(FCtx.GetTransmitting) then Result := FCtx.GetTransmitting else Result := False; +end; +function TCATEngine.SafeGetRunning: Boolean; +begin + if Assigned(FCtx.GetRunning) then Result := FCtx.GetRunning else Result := False; +end; +function TCATEngine.SafeGetSMeter: Double; +begin + if Assigned(FCtx.GetSMeter) then Result := FCtx.GetSMeter else Result := -120; +end; +function TCATEngine.SafeGetBand: Integer; +begin + if Assigned(FCtx.GetCurrentBand) then Result := FCtx.GetCurrentBand else Result := 5; +end; + +{ ── Constructor / Destructor ─────────────────────────────────────────────── } + +constructor TCATEngine.Create(const Ctx: TCATContext); +begin + inherited Create; + FCtx := Ctx; + FLock := TCriticalSection.Create; +end; + +destructor TCATEngine.Destroy; +begin + FLock.Free; + inherited; +end; + +{ ══════════════════════════════════════════════════════════════════════════ + Parse — main entry point. Thread-safe, can be called from serial/TCP threads. + ══════════════════════════════════════════════════════════════════════════ } + +function TCATEngine.Parse(const Cmd: string): string; +var + s, pfx, ext, sfx: string; + tpos: Integer; +begin + Result := ''; + s := Trim(Cmd); + // strip leading semicolons (some loggers prepend them) + while (Length(s) > 0) and (s[1] = ';') do Delete(s, 1, 1); + if Length(s) < 3 then Exit(CAT_ERROR); + + tpos := Pos(';', s); + if tpos < 3 then Exit(CAT_ERROR); + + pfx := UpperCase(Copy(s, 1, 2)); + + FLock.Acquire; + try + if pfx = 'ZZ' then + begin + if tpos < 5 then Exit(CAT_ERROR); + ext := UpperCase(Copy(s, 3, 2)); + sfx := Copy(s, 5, tpos - 5); + Result := ParseZZ(ext, sfx); + end else begin + sfx := Copy(s, 3, tpos - 3); + if pfx = 'AG' then Result := CmdAG(sfx) + else if pfx = 'AI' then Result := CmdAI(sfx) + else if pfx = 'BD' then begin CmdBD; Result := ''; end + else if pfx = 'BU' then begin CmdBU; Result := ''; end + else if pfx = 'CN' then Result := CmdCN(sfx) + else if pfx = 'CT' then Result := CmdCT(sfx) + else if pfx = 'DN' then begin CmdDN; Result := ''; end + else if pfx = 'FA' then Result := CmdFA(sfx) + else if pfx = 'FB' then Result := CmdFB(sfx) + else if pfx = 'FR' then Result := CmdFR(sfx) + else if pfx = 'FT' then Result := CmdFT(sfx) + else if pfx = 'FW' then Result := CmdFW(sfx) + else if pfx = 'GT' then Result := CmdGT(sfx) + else if pfx = 'ID' then Result := 'ID' + CmdID + ';' + else if pfx = 'IF' then Result := 'IF' + CmdIF + ';' + else if pfx = 'KS' then Result := CmdKS(sfx) + else if pfx = 'KY' then Result := CmdKY(sfx) + else if pfx = 'MD' then Result := CmdMD(sfx) + else if pfx = 'MG' then Result := CmdMG(sfx) + else if pfx = 'MO' then Result := CmdMO(sfx) + else if pfx = 'NB' then Result := CmdNB(sfx) + else if pfx = 'NT' then Result := CmdNT(sfx) + else if pfx = 'OF' then Result := CmdOF(sfx) + else if pfx = 'OS' then Result := CmdOS(sfx) + else if pfx = 'PC' then Result := CmdPC(sfx) + else if pfx = 'PR' then Result := CmdPR(sfx) + else if pfx = 'PS' then Result := CmdPS(sfx) + else if pfx = 'QI' then begin CmdQI; Result := ''; end + else if pfx = 'RC' then begin CmdRC; Result := ''; end + else if pfx = 'RD' then begin CmdRD(sfx); Result := ''; end + else if pfx = 'RT' then Result := CmdRT(sfx) + else if pfx = 'RU' then begin CmdRU(sfx); Result := ''; end + else if pfx = 'RX' then begin CmdRX(sfx); Result := ''; end + else if pfx = 'SH' then Result := CmdSH(sfx) + else if pfx = 'SL' then Result := CmdSL(sfx) + else if pfx = 'SM' then Result := CmdSM(sfx) + else if pfx = 'SQ' then Result := CmdSQ(sfx) + else if pfx = 'TX' then begin CmdTX(sfx); Result := ''; end + else if pfx = 'UP' then begin CmdUP; Result := ''; end + else if pfx = 'XT' then Result := CmdXT(sfx) + else Result := CAT_ERROR; + // Wrap standard command response + if (Result <> '') and (Result <> CAT_ERROR) and (Result <> CAT_EXEC_ERROR) then + if Copy(Result, 1, 2) <> pfx then + Result := pfx + Result + ';'; + end; + finally + FLock.Release; + end; +end; + +{ ══════════════════════════════════════════════════════════════════════════ + Standard commands (non-ZZ) + ══════════════════════════════════════════════════════════════════════════ } + +function TCATEngine.CmdAG(const s: string): string; +// AG: SET=4 chars (0xxx), GET=1 char ('0'=RX1, '1'=RX2) +// Scale 0–255 (Kenwood) ↔ 0–100 (ewsdr) +var v: Integer; +begin + if Length(s) = 4 then begin + v := Round(StrToIntDef(Copy(s,2,3), 0) / 2.55); + v := EnsureRange(v, 0, 100); + if Assigned(FCtx.SetVolume) then FCtx.SetVolume(v); + Result := ''; + end else if Length(s) = 1 then begin + v := Round(SafeGetVolume / 0.392); + Result := '0' + Pad(v, 3); + end else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdAI(const s: string): string; +// AI: auto-information — maps to ZZAI +begin + Result := ZZAI(s); +end; + +function TCATEngine.CmdBD: string; +begin + if Assigned(FCtx.DoBandDown) then FCtx.DoBandDown; + Result := ''; +end; + +function TCATEngine.CmdBU: string; +begin + if Assigned(FCtx.DoBandUp) then FCtx.DoBandUp; + Result := ''; +end; + +function TCATEngine.CmdCN(const s: string): string; +// CN: CTCSS tone — STUB: FM sub-audible tones not implemented +begin + if Length(s) = 2 then + Result := '' + else if Length(s) = 0 then + Result := '00' + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdCT(const s: string): string; +// CT: CTCSS enable — STUB: not implemented +begin + if Length(s) = 1 then + Result := '' + else if Length(s) = 0 then + Result := '0' + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdDN: string; +begin + if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown; + Result := ''; +end; + +function TCATEngine.CmdFA(const s: string): string; +// FA: VFO A frequency (11 digits, Hz) +begin + if Length(s) = 11 then begin + if Assigned(FCtx.SetVfoA) then FCtx.SetVfoA(StrToFreq(s)); + Result := ''; + end else if Length(s) = 0 then + Result := FreqToStr(SafeGetVfoA) + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdFB(const s: string): string; +// FB: VFO B frequency (11 digits, Hz) +begin + if Length(s) = 11 then begin + if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(StrToFreq(s)); + Result := ''; + end else if Length(s) = 0 then + Result := FreqToStr(SafeGetVfoB) + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdFR(const s: string): string; +// FR: receive VFO — always VFO A, dummy for compat +begin + if Length(s) = 1 then + Result := '' + else if Length(s) = 0 then + Result := '0' + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdFT(const s: string): string; +// FT: transmit VFO / split — maps to ZZSP +begin + Result := ZZSP(s); +end; + +function TCATEngine.CmdFW(const s: string): string; +// FW: filter width (4 digits) — maps to filter index +// Thetis encodes filter by index into 4-digit string 0000-0009 +var idx: Integer; +begin + if Length(s) = 4 then begin + idx := EnsureRange(StrToIntDef(s, 0), 0, 9); + if Assigned(FCtx.SetFilterIdx) then FCtx.SetFilterIdx(idx); + Result := ''; + end else if Length(s) = 0 then + Result := Pad(SafeGetFilterIdx, 4) + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdGT(const s: string): string; +// GT: AGC mode (3 digits) — 0=FAST,1=MED,2=SLOW,3=LONG,4=OFF +begin + if Length(s) = 3 then begin + if Assigned(FCtx.SetAGCMode) then FCtx.SetAGCMode(EnsureRange(StrToIntDef(s,0),0,4)); + Result := ''; + end else if Length(s) = 0 then + Result := Pad(SafeGetAGCMode, 3) + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdID: string; +begin + Result := CAT_ID_NUM; +end; + +function TCATEngine.CmdIF: string; +// IF: interface status — 37-byte composite response +var + freq, step, incr, rit, xit, tx, mode, split: string; + m: Integer; +begin + freq := FreqToStr(SafeGetVfoA); + step := '0000'; // step size — STUB: no step control + incr := '+00000'; // RIT/XIT value — STUB: no RIT/XIT + rit := '0'; + xit := '0'; + m := ModeToK(SafeGetMode); + mode := IntToStr(m); + tx := B2C(SafeGetTX); + split := '0'; // STUB: no split + Result := freq + step + incr + rit + xit + '000' + tx + mode + '0' + '0' + split + '0000'; +end; + +function TCATEngine.CmdKS(const s: string): string; +// KS: CW keying speed (3 digits, WPM) — STUB: no CW keyer +begin + if Length(s) = 3 then + Result := '' // accepted but ignored + else if Length(s) = 0 then + Result := '025' // default 25 WPM + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdKY(const s: string): string; +// KY: send CW text — STUB: no CW keyer implemented +begin + Result := '0'; // queue not full +end; + +function TCATEngine.CmdMD(const s: string): string; +// MD: mode (1 char Kenwood) — 1=LSB,2=USB,3=CW,4=FM,5=AM,7=CW-R +var k: Integer; +begin + if Length(s) = 1 then begin + k := StrToIntDef(s, 0); + if (k >= 1) and Assigned(FCtx.SetMode) then + FCtx.SetMode(KToMode(k)); + Result := ''; + end else if Length(s) = 0 then + Result := IntToStr(ModeToK(SafeGetMode)) + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdMG(const s: string): string; +// MG: mic gain (3 digits, 0-100) — STUB: TX DSP not yet implemented +begin + if Length(s) = 3 then + Result := '' + else if Length(s) = 0 then + Result := '050' + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdMO(const s: string): string; +// MO: monitor — STUB +begin + if Length(s) = 1 then Result := '' + else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.CmdNB(const s: string): string; +// NB: noise blanker on/off → maps to NBMode +begin + if Length(s) = 1 then begin + if Assigned(FCtx.SetNBMode) then + FCtx.SetNBMode(StrToIntDef(s, 0)); + Result := ''; + end else if Length(s) = 0 then + Result := IntToStr(EnsureRange(SafeGetNBMode, 0, 1)) + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdNT(const s: string): string; +// NT: notch (ANF) on/off +begin + if Length(s) = 1 then begin + if Assigned(FCtx.SetANFEnabled) then FCtx.SetANFEnabled(s = '1'); + Result := ''; + end else if Length(s) = 0 then + Result := B2C(SafeGetANF) + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdOF(const s: string): string; +// OF: FM repeater offset frequency — STUB: no FM repeater +begin + if Length(s) = 9 then Result := '' + else if Length(s) = 0 then Result := '000000000' + else Result := CAT_ERROR; +end; + +function TCATEngine.CmdOS(const s: string): string; +// OS: repeater offset direction — STUB +begin + if Length(s) = 1 then Result := '' + else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.CmdPC(const s: string): string; +// PC: TX power 0-100 % (3 digits) → DriveLevel +var v: Integer; +begin + if Length(s) = 3 then begin + v := EnsureRange(StrToIntDef(s,0), 0, 100); + if Assigned(FCtx.SetDriveLevel) then FCtx.SetDriveLevel(v); + Result := ''; + end else if Length(s) = 0 then + Result := Pad(SafeGetDriveLevel, 3) + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdPR(const s: string): string; +// PR: speech compressor — STUB: always off +begin + if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.CmdPS(const s: string): string; +// PS: power status +begin + if Length(s) = 1 then Result := '' // accept but ignore + else if Length(s) = 0 then + Result := B2C(SafeGetRunning) + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdQI: string; +// QI: store VFO A to quick memory — STUB +begin + Result := ''; +end; + +function TCATEngine.CmdRC: string; +// RC: clear RIT — STUB: no RIT +begin + Result := ''; +end; + +function TCATEngine.CmdRD(const s: string): string; +// RD: decrease RIT — maps to tune down +begin + if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown; + Result := ''; +end; + +function TCATEngine.CmdRT(const s: string): string; +// RT: RIT on/off — STUB: no RIT +begin + if Length(s) = 1 then Result := '' + else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.CmdRU(const s: string): string; +// RU: increase RIT — maps to tune up +begin + if Assigned(FCtx.DoTuneUp) then FCtx.DoTuneUp; + Result := ''; +end; + +function TCATEngine.CmdRX(const s: string): string; +// RX: switch to receive +begin + if Assigned(FCtx.SetTransmitting) then FCtx.SetTransmitting(False); + Result := ''; +end; + +function TCATEngine.CmdSH(const s: string): string; +// SH: high pass filter setting (2 digits, 0-based index) — maps to filter index +var idx: Integer; +begin + if Length(s) = 2 then begin + idx := EnsureRange(StrToIntDef(s, 0), 0, 9); + if Assigned(FCtx.SetFilterIdx) then FCtx.SetFilterIdx(idx); + Result := ''; + end else if Length(s) = 0 then + Result := Pad(SafeGetFilterIdx, 2) + else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdSL(const s: string): string; +// SL: low pass filter setting — mirrors SH +begin + Result := CmdSH(s); +end; + +function TCATEngine.CmdSM(const s: string): string; +// SM: S-meter read (1 char VFO selector, response 4 chars 0000-0015) +// Kenwood SM answer: 0000–0015 → S0–S9+60dB +// We return 4-digit raw 0000-0030 compatible format +var sm: Integer; +begin + if (Length(s) = 1) and ((s = '0') or (s = '1')) then begin + sm := SMeterRaw(SafeGetSMeter); + // Scale 0-260 → 0-30 (Kenwood SM scale 0000-0030) + sm := Round(sm * 30 / 260); + Result := Pad(sm, 4); + end else + Result := CAT_ERROR; +end; + +function TCATEngine.CmdSQ(const s: string): string; +// SQ: squelch — STUB: no squelch control +begin + if Length(s) = 4 then Result := '' + else if Length(s) = 1 then Result := '0000' + else Result := CAT_ERROR; +end; + +function TCATEngine.CmdTX(const s: string): string; +// TX: switch to transmit +begin + if Assigned(FCtx.SetTransmitting) then FCtx.SetTransmitting(True); + Result := ''; +end; + +function TCATEngine.CmdUP: string; +begin + if Assigned(FCtx.DoTuneUp) then FCtx.DoTuneUp; + Result := ''; +end; + +function TCATEngine.CmdXT(const s: string): string; +// XT: XIT on/off — STUB: no XIT +begin + if Length(s) = 1 then Result := '' + else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +{ ══════════════════════════════════════════════════════════════════════════ + ZZ* extended commands dispatcher + ══════════════════════════════════════════════════════════════════════════ } + +function TCATEngine.ParseZZ(const ext, s: string): string; +var ans: string; +begin + ans := ''; + if ext = 'AA' then ans := ZZAA(s) + else if ext = 'AB' then ans := ZZAB(s) + else if ext = 'AC' then ans := ZZAC(s) + else if ext = 'AD' then ans := '' // STUB: TX freq display + else if ext = 'AE' then ans := '' // STUB: RIT value + else if ext = 'AF' then ans := '' // STUB: XIT value + else if ext = 'AG' then ans := ZZAG(s) + else if ext = 'AI' then ans := ZZAI(s) + else if ext = 'AP' then ans := ZZAP(s) + else if ext = 'AR' then ans := ZZAR(s) + else if ext = 'AS' then ans := ZZAS(s) + else if ext = 'AT' then ans := ZZAT(s) + else if ext = 'AU' then ans := ZZFA(s) // alias ZZFA + else if ext = 'AY' then ans := '' // STUB: APF type + else if ext = 'BA' then ans := ZZBA + else if ext = 'BB' then ans := '' // STUB: band button state + else if ext = 'BD' then begin ZZBD; ans := ''; end + else if ext = 'BE' then ans := ZZBE(s) + else if ext = 'BF' then ans := '' // STUB: sub-RX filter + else if ext = 'BG' then ans := '' // STUB: sub-RX gain + else if ext = 'BI' then ans := ZZBI(s) + else if ext = 'BM' then ans := ZZBM(s) + else if ext = 'BP' then ans := ZZAP(s) // alias ZZAP + else if ext = 'BR' then ans := '' // STUB: sub-RX enable + else if ext = 'BS' then ans := ZZBS(s) + else if ext = 'BT' then ans := '' // STUB: band TX + else if ext = 'BU' then begin ZZBU; ans := ''; end + else if ext = 'BY' then ans := '' // STUB: VFO bypass + else if ext = 'CB' then ans := ZZCB(s) + else if ext = 'CD' then ans := ZZCD(s) + else if ext = 'CF' then ans := '' // STUB: compressor freq + else if ext = 'CI' then ans := '' // STUB: compressor in + else if ext = 'CL' then ans := '' // STUB: compressor level + else if ext = 'CM' then ans := '' // STUB: compressor mode + else if ext = 'CN' then ans := ZZCN(s) + else if ext = 'CO' then ans := '' // STUB: compressor out + else if ext = 'CP' then ans := '' // STUB: compressor peak + else if ext = 'CS' then ans := ZZCS(s) + else if ext = 'CT' then ans := '' // STUB: compressor threshold + else if ext = 'CU' then begin ZZDU; ans := ''; end + else if ext = 'DA' then ans := ZZDA(s) + else if ext = 'DB' then ans := ZZDB(s) + else if ext = 'DC' then ans := '' // STUB: display contrast + else if ext = 'DD' then ans := '' // STUB: DSP disable + else if ext = 'DE' then ans := '' // STUB: DSP enable + else if ext = 'DF' then ans := '' // STUB: TX filter low + else if ext = 'DG' then ans := '' // STUB: TX filter high + else if ext = 'DH' then ans := '' // STUB: TX DSP high cut + else if ext = 'DM' then ans := ZZDM(s) + else if ext = 'DN' then ans := ZZDN(s) + else if ext = 'DO' then ans := '' // STUB: sub-RX on + else if ext = 'DP' then ans := '' // STUB: display position + else if ext = 'DQ' then ans := '' // STUB: sub-RX + else if ext = 'DR' then ans := '' // STUB: sub-RX + else if ext = 'DU' then begin ZZDU; ans := ''; end + else if ext = 'DX' then ans := '' // STUB: sub NB + else if ext = 'EA' then ans := ZZEA(s) + else if ext = 'EB' then ans := ZZEB(s) + else if ext = 'EM' then ans := ZZEM(s) + else if ext = 'ER' then ans := '' // STUB: EQ reset + else if ext = 'ET' then ans := '' // STUB: EQ TX + else if ext = 'FA' then ans := ZZFA(s) + else if ext = 'FB' then ans := ZZFB(s) + else if ext = 'FD' then ans := ZZFD(s) + else if ext = 'FH' then ans := ZZFH(s) + else if ext = 'FI' then ans := ZZFI(s) + else if ext = 'FJ' then ans := '' // STUB: RX2 filter + else if ext = 'FL' then ans := ZZFL(s) + else if ext = 'FM' then ans := ZZFM + else if ext = 'FR' then ans := '' // STUB: filter RX + else if ext = 'FS' then ans := '' // STUB: sub filter + else if ext = 'FT' then ans := ZZFT(s) + else if ext = 'FV' then ans := '' // STUB: filter var + else if ext = 'FW' then ans := '' // STUB: filter write + else if ext = 'FX' then ans := '' // STUB: filter extended + else if ext = 'FY' then ans := '' // STUB: filter Y + else if ext = 'GA' then ans := ZZGA(s) + else if ext = 'GE' then ans := '' // STUB: gain enable + else if ext = 'GL' then ans := '' // STUB: gain level + else if ext = 'GR' then ans := ZZGR(s) + else if ext = 'GT' then ans := ZZGT(s) + else if ext = 'GU' then ans := '' // STUB: gain update + else if ext = 'HA' then ans := '' // STUB: hardware A + else if ext = 'HR' then ans := '' // STUB: hardware read + else if ext = 'HT' then ans := '' // STUB: hardware TX + else if ext = 'HU' then ans := '' // STUB: hardware update + else if ext = 'HV' then ans := '' // STUB: hardware value + else if ext = 'HW' then ans := '' // STUB: hardware write + else if ext = 'HX' then ans := '' // STUB: hardware extended + else if ext = 'ID' then ans := ZZID + else if ext = 'IF' then ans := ZZIF(s) + else if ext = 'IO' then ans := ZZIO + else if ext = 'IS' then ans := ZZIS(s) + else if ext = 'IT' then ans := '' // STUB: initial TX + else if ext = 'IU' then ans := '' // STUB: initial update + else if ext = 'JO' then ans := '' // STUB: JSON + else if ext = 'JP' then ans := '' // STUB: JSON preset + else if ext = 'JQ' then ans := '' // STUB: JSON query + else if ext = 'JR' then ans := '' // STUB: JSON read + else if ext = 'JS' then ans := '' // STUB: JSON status + else if ext = 'KM' then ans := ZZKM(s) + else if ext = 'KO' then ans := '' // STUB: key on + else if ext = 'KS' then ans := ZZKS(s) + else if ext = 'KY' then ans := ZZKY(s) + else if ext = 'LA' then ans := '' // STUB: level A + else if ext = 'LB' then ans := '' // STUB: level B + else if ext = 'LC' then ans := '' // STUB: level C + else if ext = 'LD' then ans := '' // STUB: level D + else if ext = 'LE' then ans := '' // STUB: level E + else if ext = 'LF' then ans := '' // STUB: level F + else if ext = 'LG' then ans := '' // STUB: level G + else if ext = 'LH' then ans := '' // STUB: level H + else if ext = 'LI' then ans := '' // STUB: level I + else if ext = 'MA' then ans := ZZMA(s) + else if ext = 'MB' then ans := ZZMB(s) + else if ext = 'MD' then ans := ZZMD(s) + else if ext = 'ME' then ans := '' // STUB: mode enable + else if ext = 'MF' then ans := '' // STUB: mode freq + else if ext = 'MG' then ans := ZZMG(s) + else if ext = 'ML' then ans := ZZML + else if ext = 'MN' then ans := ZZMN(s) + else if ext = 'MO' then ans := ZZMO(s) + else if ext = 'MR' then ans := '' // STUB: mode RX + else if ext = 'MS' then ans := '' // STUB: mode status + else if ext = 'MT' then ans := '' // STUB: mode TX + else if ext = 'MU' then ans := '' // STUB: mode update + else if ext = 'MV' then ans := ZZMV + else if ext = 'MW' then ans := '' // STUB: mode write + else if ext = 'MX' then ans := '' // STUB: mode extended + else if ext = 'MY' then ans := ZZMY + else if ext = 'MZ' then ans := '' // STUB: mode Z + else if ext = 'NA' then ans := ZZNA(s) + else if ext = 'NB' then ans := ZZNB(s) + else if ext = 'NC' then ans := '' // STUB: NR C + else if ext = 'ND' then ans := '' // STUB: NR D + else if ext = 'NE' then ans := '' // STUB: NR E + else if ext = 'NF' then ans := '' // STUB: NR F + else if ext = 'NG' then ans := '' // STUB: noise gate + else if ext = 'NH' then ans := '' // STUB: NR H + else if ext = 'NL' then ans := ZZNL(s) + else if ext = 'NM' then ans := '' // STUB: NR mode + else if ext = 'NN' then ans := ZZNN(s) + else if ext = 'NO' then ans := '' // STUB: sub NR + else if ext = 'NR' then ans := ZZNR(s) + else if ext = 'NS' then ans := ZZNS(s) + else if ext = 'NT' then ans := ZZNT(s) + else if ext = 'NU' then ans := '' // STUB: NR update + else if ext = 'NV' then ans := '' // STUB: NR value + else if ext = 'NW' then ans := '' // STUB: NR write + else if ext = 'OA' then ans := ZZOA(s) + else if ext = 'OB' then ans := ZZOB(s) + else if ext = 'OC' then ans := '' // STUB: output compression + else if ext = 'OD' then ans := '' // STUB: output DSP + else if ext = 'OE' then ans := '' // STUB: output E + else if ext = 'OF' then ans := '' // STUB: sub-RX freq + else if ext = 'OG' then ans := '' // STUB: output gain + else if ext = 'OH' then ans := '' // STUB: output high + else if ext = 'OJ' then ans := '' // STUB: output J + else if ext = 'OL' then ans := '' // STUB: output level + else if ext = 'OS' then ans := '' // STUB: output status + else if ext = 'OT' then ans := '' // STUB: FM repeater offset + else if ext = 'OU' then ans := '' // STUB: output update + else if ext = 'OV' then ans := '' // STUB: output value + else if ext = 'OW' then ans := '' // STUB: output write + else if ext = 'OX' then ans := '' // STUB: oxide extra + else if ext = 'OZ' then ans := '' // STUB: oxide Z + else if ext = 'PA' then ans := '' // STUB: sub-RX pan + else if ext = 'PB' then ans := '' // STUB: power B + else if ext = 'PC' then ans := ZZPC(s) + else if ext = 'PD' then ans := ZZPD + else if ext = 'PE' then ans := '' // STUB: power enable + else if ext = 'PO' then ans := ZZPO(s) + else if ext = 'PS' then ans := ZZPS(s) + else if ext = 'PY' then ans := '' // STUB: power Y + else if ext = 'PZ' then ans := '' // STUB: power Z + else if ext = 'QK' then ans := '' // STUB: query keep + else if ext = 'QM' then ans := ZZQM + else if ext = 'QR' then ans := ZZQR + else if ext = 'QS' then ans := ZZQS + else if ext = 'RA' then ans := ZZRA(s) + else if ext = 'RB' then ans := '' // STUB: RIT B + else if ext = 'RC' then begin ZZRC; ans := ''; end + else if ext = 'RD' then begin ZZRD(s); ans := ''; end + else if ext = 'RF' then ans := '' // STUB: RIT freq + else if ext = 'RH' then ans := '' // STUB: RIT high + else if ext = 'RL' then ans := ZZRL(s) + else if ext = 'RM' then ans := '' // STUB: RIT mode + else if ext = 'RS' then ans := '' // STUB: RIT status + else if ext = 'RT' then ans := '' // STUB: RIT transmit + else if ext = 'RU' then begin ZZRU(s); ans := ''; end + else if ext = 'RV' then ans := ZZRV + else if ext = 'RX' then ans := ZZRX(s) + else if ext = 'RY' then ans := '' // STUB: RIT Y + else if ext = 'SA' then begin ZZSA; ans := ''; end + else if ext = 'SB' then begin ZZSB; ans := ''; end + else if ext = 'SD' then ans := '' // STUB: sub-RX display + else if ext = 'SF' then ans := '' // STUB: sweep freq + else if ext = 'SG' then ans := '' // STUB: sub gain + else if ext = 'SH' then ans := '' // STUB: sub high cut + else if ext = 'SM' then ans := ZZSM(s) + else if ext = 'SN' then ans := '' // STUB: serial number + else if ext = 'SO' then ans := ZZSO(s) + else if ext = 'SP' then ans := ZZSP(s) + else if ext = 'SQ' then ans := ZZSQ(s) + else if ext = 'SR' then ans := ZZSR(s) + else if ext = 'SS' then ans := '' // STUB: sub-RX + else if ext = 'ST' then begin ZZST; ans := ''; end + else if ext = 'SU' then begin ZZSU; ans := ''; end + else if ext = 'SV' then ans := '' // STUB: spectrum value + else if ext = 'SW' then ans := '' // STUB: spectrum write + else if ext = 'SX' then ans := '' // STUB: spectrum X + else if ext = 'SY' then ans := '' // STUB: spectrum Y + else if ext = 'SZ' then ans := '' // STUB: spectrum Z + else if ext = 'TA' then ans := ZZTA(s) + else if ext = 'TB' then ans := ZZTB(s) + else if ext = 'TF' then ans := '' // STUB: tone freq + else if ext = 'TH' then ans := '' // STUB: tone high + else if ext = 'TI' then ans := '' // STUB: tone input + else if ext = 'TL' then ans := '' // STUB: tone level + else if ext = 'TM' then ans := '' // STUB: tone mode + else if ext = 'TO' then ans := '' // STUB: tone on/off + else if ext = 'TP' then ans := '' // STUB: tone preset + else if ext = 'TS' then ans := '' // STUB: tone status + else if ext = 'TU' then ans := '' // STUB: tone update + else if ext = 'TV' then ans := '' // STUB: tone value + else if ext = 'TX' then ans := ZZTX(s) + else if ext = 'UA' then ans := '' // STUB: update A + else if ext = 'UP' then ans := ZZUP(s) + else if ext = 'US' then ans := ZZUS + else if ext = 'UT' then ans := ZZUT(s) + else if ext = 'UX' then ans := ZZUX(s) + else if ext = 'UY' then ans := ZZUY(s) + else if ext = 'VA' then ans := ZZVA(s) + else if ext = 'VB' then ans := ZZVB(s) + else if ext = 'VC' then ans := '' // STUB: voice compression + else if ext = 'VD' then ans := '' // STUB: VFO DSP + else if ext = 'VE' then ans := '' // STUB: VFO enable + else if ext = 'VF' then ans := ZZFA(s) // alias ZZFA + else if ext = 'VG' then ans := ZZVG(s) + else if ext = 'VH' then ans := '' // STUB: VFO high + else if ext = 'VI' then ans := '' // STUB: VFO input + else if ext = 'VJ' then ans := '' // STUB: VFO J + else if ext = 'VK' then ans := '' // STUB: VFO K + else if ext = 'VL' then ans := '' // STUB: VFO level + else if ext = 'VM' then ans := '' // STUB: VFO mode + else if ext = 'VN' then ans := ZZVN + else if ext = 'VO' then ans := '' // STUB: VFO on/off + else if ext = 'VP' then ans := '' // STUB: VFO preset + else if ext = 'VQ' then ans := '' // STUB: VFO Q + else if ext = 'VR' then ans := '' // STUB: VFO read + else if ext = 'VS' then ans := ZZVS(s) + else if ext = 'VT' then ans := '' // STUB: VFO TX + else if ext = 'VU' then ans := '' // STUB: VFO update + else if ext = 'VV' then ans := '' // STUB: VFO value + else if ext = 'VW' then ans := '' // STUB: VFO write + else if ext = 'VX' then ans := '' // STUB: VFO extended + else if ext = 'VY' then ans := '' // STUB: VFO Y + else if ext = 'VZ' then ans := '' // STUB: VFO Z + else if ext = 'WA' then ans := ZZWA(s) + else if ext = 'WB' then ans := ZZWB(s) + else if ext = 'WC' then ans := ZZWC(s) + else if ext = 'WD' then ans := ZZWD(s) + else if ext = 'WE' then ans := ZZWE(s) + else if ext = 'WF' then ans := ZZWF(s) + else if ext = 'WG' then ans := ZZWG(s) + else if ext = 'WH' then ans := ZZWH(s) + else if ext = 'WJ' then ans := ZZWJ(s) + else if ext = 'WK' then ans := ZZWK(s) + else if ext = 'WL' then ans := ZZWL(s) + else if ext = 'WM' then ans := ZZWM(s) + else if ext = 'WN' then ans := ZZWN(s) + else if ext = 'WO' then ans := ZZWO(s) + else if ext = 'WP' then ans := ZZWP(s) + else if ext = 'WQ' then ans := ZZWQ(s) + else if ext = 'WR' then ans := ZZWR(s) + else if ext = 'WS' then ans := ZZWS(s) + else if ext = 'WT' then ans := '' // STUB: waterfall T + else if ext = 'WU' then ans := '' // STUB: waterfall U + else if ext = 'WV' then ans := '' // STUB: waterfall V + else if ext = 'WW' then ans := '' // STUB: waterfall W + else if ext = 'XA' then ans := ZZXA(s) + else if ext = 'XC' then ans := ZZXC + else if ext = 'XD' then ans := '' // STUB: extended D + else if ext = 'XF' then ans := ZZXF(s) + else if ext = 'XH' then ans := '' // STUB: extended H + else if ext = 'XN' then ans := '' // STUB: extended N + else if ext = 'XO' then ans := '' // STUB: extended O + else if ext = 'XS' then ans := '' // STUB: extended S + else if ext = 'XT' then ans := '' // STUB: extended T + else if ext = 'XU' then ans := '' // STUB: extended U + else if ext = 'XV' then ans := '' // STUB: extended V + else if ext = 'YA' then ans := ZZYA(s) + else if ext = 'YB' then ans := ZZYB(s) + else if ext = 'YC' then ans := ZZYC(s) + else if ext = 'YR' then ans := ZZYR(s) + else if ext = 'ZA' then ans := ZZZA(s) + else if ext = 'ZB' then begin ZZZB; ans := ''; end + else if ext = 'ZD' then ans := '' // STUB: system D + else if ext = 'ZE' then ans := '' // STUB: system E + else if ext = 'ZM' then ans := ZZZM(s) + else if ext = 'ZN' then ans := '' // STUB: system N + else if ext = 'ZO' then ans := ZZZO(s) + else if ext = 'ZP' then ans := ZZZP(s) + else if ext = 'ZQ' then ans := ZZZQ(s) + else if ext = 'ZR' then ans := ZZZR(s) + else if ext = 'ZS' then ans := ZZZS(s) + else if ext = 'ZT' then ans := ZZZT(s) + else if ext = 'ZU' then ans := ZZZU(s) + else if ext = 'ZV' then ans := ZZZV(s) + else if ext = 'ZW' then ans := ZZZW(s) + else if ext = 'ZZ' then begin ZZZZ; ans := ''; end + else begin Result := CAT_ERROR; Exit; end; + + if (ans = '') or (ans = CAT_ERROR) or (ans = CAT_EXEC_ERROR) then + Result := ans + else + Result := 'ZZ' + ext + s + ans + ';'; +end; + +{ ══════════════════════════════════════════════════════════════════════════ + ZZ command implementations + ══════════════════════════════════════════════════════════════════════════ } + +function TCATEngine.ZZAA(const s: string): string; +// ZZAA: RX1 AF gain — alias ZZAG +begin Result := ZZAG(s); end; + +function TCATEngine.ZZAB(const s: string): string; +// ZZAB: RX2 AF gain — STUB: no RX2 +begin + if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZAC(const s: string): string; +// ZZAC: band selection (2-digit band index) +var idx: Integer; +begin + if Length(s) = 2 then begin + idx := EnsureRange(StrToIntDef(s, 0), 0, 10); + if Assigned(FCtx.DoBandByIndex) then FCtx.DoBandByIndex(idx); + Result := ''; + end else if Length(s) = 0 then + Result := Pad(SafeGetBand, 2) + else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZAG(const s: string): string; +// ZZAG: AF gain 0–100 +var v: Integer; +begin + if Length(s) = 3 then begin + v := EnsureRange(StrToIntDef(s, 0), 0, 100); + if Assigned(FCtx.SetVolume) then FCtx.SetVolume(v); + Result := ''; + end else if Length(s) = 0 then + Result := Pad(SafeGetVolume, 3) + else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZAI(const s: string): string; +// ZZAI: auto-information broadcast — STUB: not applicable for CAT-only mode +begin + if Length(s) = 1 then Result := '' + else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZAP(const s: string): string; +// ZZAP: TX power 0–100 +begin Result := ZZPC(s); end; + +function TCATEngine.ZZAR(const s: string): string; +// ZZAR: RIT enable — 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.ZZAS(const s: string): string; +// ZZAS: sub-RX enable — STUB: no RX2 +begin + if Length(s) = 1 then Result := '' + else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZAT(const s: string): string; +// ZZAT: antenna tuner — STUB: no ATU +begin + if Length(s) = 1 then Result := '' + else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZBA: string; +// ZZBA: sub-RX AF gain — STUB: no RX2 +begin Result := '050'; end; + +function TCATEngine.ZZBD: string; +// ZZBD: band down +begin + if Assigned(FCtx.DoBandDown) then FCtx.DoBandDown; + Result := ''; +end; + +function TCATEngine.ZZBE(const s: string): string; +// ZZBE: band enable — STUB: all bands always enabled +begin + if Length(s) = 3 then Result := '' + else if Length(s) = 2 then Result := '1' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZBI(const s: string): string; +// ZZBI: filter index (2-digit, 0-9) +var idx: Integer; +begin + if Length(s) = 2 then begin + idx := EnsureRange(StrToIntDef(s, 0), 0, 9); + if Assigned(FCtx.SetFilterIdx) then FCtx.SetFilterIdx(idx); + Result := ''; + end else if Length(s) = 0 then + Result := Pad(SafeGetFilterIdx, 2) + else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZBM(const s: string): string; +// ZZBM: band mode — set mode for current band +begin Result := ZZMD(s); end; + +function TCATEngine.ZZBS(const s: string): string; +// ZZBS: band select — same as ZZAC +begin Result := ZZAC(s); end; + +function TCATEngine.ZZBU: string; +begin + if Assigned(FCtx.DoBandUp) then FCtx.DoBandUp; + Result := ''; +end; + +function TCATEngine.ZZCB(const s: string): string; +// ZZCB: compression bypass — STUB: TX DSP not implemented +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZCD(const s: string): string; +// ZZCD: compressor drive — STUB: TX DSP not implemented +begin + if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZCN(const s: string): string; +// ZZCN: compressor on — STUB: TX DSP not implemented +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZCS(const s: string): string; +// ZZCS: compressor status — STUB +begin + if Length(s) = 0 then Result := '0' else Result := CAT_ERROR; +end; + +function TCATEngine.ZZDA(const s: string): string; +// ZZDA: diversity A — STUB: diversity not implemented +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZDB(const s: string): string; +// ZZDB: diversity B — STUB +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZDM(const s: string): string; +// ZZDM: display mode — STUB: no separate display mode setting +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZDN(const s: string): string; +// ZZDN: drive level — alias ZZPC +begin Result := ZZPC(s); end; + +function TCATEngine.ZZDU: string; +// ZZDU: display units update — STUB +begin Result := ''; end; + +function TCATEngine.ZZEM(const s: string): string; +// ZZEM: error message — returns error code info (Verbose mode) +begin Result := CAT_ERROR; end; + +function TCATEngine.ZZEA(const s: string): string; +// ZZEA: RX EQ profile name — STUB: no EQ +begin + if Length(s) > 0 then Result := '' else Result := ''; // no EQ +end; + +function TCATEngine.ZZEB(const s: string): string; +// ZZEB: TX EQ profile name — STUB: TX DSP not implemented +begin + if Length(s) > 0 then Result := '' else Result := ''; +end; + +function TCATEngine.ZZFA(const s: string): string; +// ZZFA: VFO A frequency (11 digits, Hz) +begin + if Length(s) = 11 then begin + if Assigned(FCtx.SetVfoA) then FCtx.SetVfoA(StrToFreq(s)); + Result := ''; + end else if Length(s) = 0 then + Result := FreqToStr(SafeGetVfoA) + else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZFB(const s: string): string; +// ZZFB: VFO B frequency +begin + if Length(s) = 11 then begin + if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(StrToFreq(s)); + Result := ''; + end else if Length(s) = 0 then + Result := FreqToStr(SafeGetVfoB) + else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZFD(const s: string): string; +// ZZFD: FM deviation — STUB: FM deviation not configurable +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '1' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZFI(const s: string): string; +// ZZFI: filter index (2-digit) +begin Result := ZZBI(s); end; + +function TCATEngine.ZZFL(const s: string): string; +// ZZFL: filter low cut (signed Hz, e.g. "-150") +// STUB: ewsdr uses indexed filters, not arbitrary Hz cutoffs +begin + if Length(s) >= 4 then + Result := '' // accept but ignore — STUB + else if Length(s) = 0 then begin + if SafeGetMode in [MODE_CWL, MODE_CWU] then Result := '-0500' + else Result := '-1500'; + end else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZFH(const s: string): string; +// ZZFH: filter high cut (signed Hz) +// STUB: ewsdr uses indexed filters, not arbitrary Hz cutoffs +begin + if Length(s) >= 4 then + Result := '' // accept but ignore — STUB + else if Length(s) = 0 then begin + if SafeGetMode in [MODE_CWL, MODE_CWU] then Result := '+0500' + else Result := '+3000'; + end else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZFM: string; +// ZZFM: filter mode — read-only, returns current filter index as string +begin + Result := Pad(SafeGetFilterIdx, 2); +end; + +function TCATEngine.ZZFT(const s: string): string; +// ZZFT: TX frequency — same as ZZFA (no split yet) +begin Result := ZZFA(s); end; + +function TCATEngine.ZZGA(const s: string): string; +// ZZGA: GUID add (TCP client ID management) — handled by TCP server layer +// If called from serial, just acknowledge +begin + if Length(s) = 36 then Result := s + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZGR(const s: string): string; +// ZZGR: GUID remove (TCP client ID) — handled by TCP server layer +begin + if Length(s) = 36 then Result := s + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZGT(const s: string): string; +// ZZGT: AGC mode (1 char: 0=FAST,1=MED,2=SLOW,3=LONG,4=OFF) → 3-digit response +var m: Integer; +begin + if Length(s) = 1 then begin + m := EnsureRange(StrToIntDef(s, 0), 0, 4); + if Assigned(FCtx.SetAGCMode) then FCtx.SetAGCMode(m); + Result := ''; + end else if Length(s) = 0 then + Result := Pad(SafeGetAGCMode, 3) + else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZID: string; +begin Result := CAT_ID_NUM; end; + +function TCATEngine.ZZIF(const s: string): string; +// ZZIF: IF — STUB: secondary interface (returns same as IF) +begin + if Length(s) = 0 then Result := CmdIF + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZIO: string; +// ZZIO: I/O status — STUB +begin Result := '0'; end; + +function TCATEngine.ZZIS(const s: string): string; +// ZZIS: initial setup flag — STUB +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '1' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZKM(const s: string): string; +// ZZKM: CW key mode — STUB: no CW keyer +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZKS(const s: string): string; +// ZZKS: CW speed (WPM) — STUB: no CW keyer +begin + if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '025' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZKY(const s: string): string; +// ZZKY: CW text to send — STUB: no CW keyer implemented +begin + Result := '0'; // buffer not full +end; + +function TCATEngine.ZZMA(const s: string): string; +// ZZMA: RX1 mode +begin Result := ZZMD(s); end; + +function TCATEngine.ZZMB(const s: string): string; +// ZZMB: RX2 mode — STUB: no RX2; returns same as RX1 +begin Result := ZZMD(s); end; + +function TCATEngine.ZZMD(const s: string): string; +// ZZMD: mode (1 digit Kenwood code, same as MD) +var k: Integer; +begin + if Length(s) = 1 then begin + k := StrToIntDef(s, 0); + if (k >= 1) and Assigned(FCtx.SetMode) then FCtx.SetMode(KToMode(k)); + Result := ''; + end else if Length(s) = 0 then + Result := IntToStr(ModeToK(SafeGetMode)) + else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZMG(const s: string): string; +// ZZMG: microphone gain 0–70 (Thetis scale) — STUB: TX DSP not implemented +begin + if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZML: string; +// ZZML: mode list (returns all mode names) +begin Result := 'LSB USB DSB CWL CWU FM AM SAM '; end; + +function TCATEngine.ZZMN(const s: string): string; +// ZZMN: mode name — returns mode name for given mode number +var k: Integer; + names: array[0..7] of string; +begin + names[0]:='LSB'; names[1]:='USB'; names[2]:='DSB'; names[3]:='CWL'; + names[4]:='CWU'; names[5]:='FM'; names[6]:='AM'; names[7]:='SAM'; + if Length(s) = 1 then begin + k := EnsureRange(StrToIntDef(s,0),0,7); + Result := names[k]; + end else Result := CAT_ERROR; +end; + +function TCATEngine.ZZMO(const s: string): string; +// ZZMO: monitor on/off — STUB: no TX monitor +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZMV: string; +// ZZMV: mode value — returns current mode index (ewsdr 0-7) +begin Result := Pad(SafeGetMode, 2); end; + +function TCATEngine.ZZMY: string; +// ZZMY: mode Y — STUB +begin Result := ''; end; + +function TCATEngine.ZZNA(const s: string): string; +// ZZNA: noise blanker A (NB1) on/off +var v: Integer; +begin + if Length(s) = 1 then begin + v := StrToIntDef(s, 0); + if Assigned(FCtx.SetNBMode) then FCtx.SetNBMode(v); + Result := ''; + end else if Length(s) = 0 then + Result := IntToStr(EnsureRange(SafeGetNBMode,0,1)) + else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZNB(const s: string): string; +// ZZNB: noise blanker mode (extended, 0-4) +var v: Integer; +begin + if Length(s) = 1 then begin + v := EnsureRange(StrToIntDef(s,0),0,4); + if Assigned(FCtx.SetNBMode) then FCtx.SetNBMode(v); + Result := ''; + end else if Length(s) = 0 then + Result := IntToStr(SafeGetNBMode) + else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZNL(const s: string): string; +// ZZNL: NB level — STUB: NB level not configurable +begin + if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZNN(const s: string): string; +// ZZNN: notch (manual) on/off — STUB: no manual notch +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZNR(const s: string): string; +// ZZNR: noise reduction on/off (0=off, 1-4=NR mode) +var v: Integer; +begin + if Length(s) = 1 then begin + v := EnsureRange(StrToIntDef(s,0),0,4); + if Assigned(FCtx.SetNRMode) then FCtx.SetNRMode(v); + Result := ''; + end else if Length(s) = 0 then + Result := IntToStr(SafeGetNRMode) + else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZNS(const s: string): string; +// ZZNS: spectral noise blanker (SNB) on/off +begin + if Length(s) = 1 then begin + if Assigned(FCtx.SetSNBEnabled) then FCtx.SetSNBEnabled(s = '1'); + Result := ''; + end else if Length(s) = 0 then + Result := B2C(SafeGetSNB) + else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZNT(const s: string): string; +// ZZNT: noise blanker type — STUB: maps to NB mode +begin Result := ZZNB(s); end; + +function TCATEngine.ZZOA(const s: string): string; +// ZZOA: RX1 output level — alias ZZAG +begin Result := ZZAG(s); end; + +function TCATEngine.ZZOB(const s: string): string; +// ZZOB: RX2 output level — STUB: no RX2 +begin + if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZPC(const s: string): string; +// ZZPC: TX power 0–100 → DriveLevel +var v: Integer; +begin + if Length(s) = 3 then begin + v := EnsureRange(StrToIntDef(s,0),0,100); + if Assigned(FCtx.SetDriveLevel) then FCtx.SetDriveLevel(v); + Result := ''; + end else if Length(s) = 0 then + Result := Pad(SafeGetDriveLevel, 3) + else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZPD: string; +// ZZPD: power default — return nominal 100 +begin Result := '100'; end; + +function TCATEngine.ZZPO(const s: string): string; +// ZZPO: power on/off — STUB: always on when running +begin + if Length(s) = 1 then Result := '' + else if Length(s) = 0 then Result := B2C(SafeGetRunning) + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZPS(const s: string): string; +// ZZPS: power status +begin + if Length(s) = 1 then Result := '' + else if Length(s) = 0 then Result := B2C(SafeGetRunning) + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZQM: string; +// ZZQM: current mode query +begin Result := IntToStr(ModeToK(SafeGetMode)); end; + +function TCATEngine.ZZQR: string; +// ZZQR: quick recall — STUB +begin Result := ''; end; + +function TCATEngine.ZZQS: string; +// ZZQS: quick store — STUB +begin Result := ''; end; + +function TCATEngine.ZZRA(const s: string): string; +// ZZRA: RIT A value — STUB: no RIT +begin + if Length(s) = 5 then Result := '' else if Length(s) = 0 then Result := '+0000' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZRC: string; +begin Result := ''; end; // clear RIT — STUB + +function TCATEngine.ZZRD(const s: string): string; +// ZZRD: tune/RIT down +begin + if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown; + Result := ''; +end; + +function TCATEngine.ZZRL(const s: string): string; +// ZZRL: RF gain — STUB: no RF gain control +begin + if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '100' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZRU(const s: string): string; +// ZZRU: tune/RIT up +begin + if Assigned(FCtx.DoTuneUp) then FCtx.DoTuneUp; + Result := ''; +end; + +function TCATEngine.ZZRV: string; +// ZZRV: software version +begin Result := '001000'; end; // version 0.1.0 + +function TCATEngine.ZZRX(const s: string): string; +// ZZRX: switch to receive +begin + if Assigned(FCtx.SetTransmitting) then FCtx.SetTransmitting(False); + if Length(s) = 0 then Result := B2C(not SafeGetTX) + else Result := ''; +end; + +function TCATEngine.ZZSA: string; +// ZZSA: VFO tune down (maps to step down) +begin + if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown; + Result := ''; +end; + +function TCATEngine.ZZSB: string; +// ZZSB: VFO tune up (maps to step up) +begin + if Assigned(FCtx.DoTuneUp) then FCtx.DoTuneUp; + Result := ''; +end; + +function TCATEngine.ZZSM(const s: string): string; +// ZZSM: S-meter (0=RX1, 1=RX2) +// Response: 3-digit 000-260 (calculated as (dBm+140)*2) +var sm: Integer; +begin + if (Length(s) = 1) and ((s = '0') or (s = '1')) then begin + sm := SMeterRaw(SafeGetSMeter); + Result := Pad(sm, 3); + end else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZSO(const s: string): string; +// ZZSO: sub-RX on — STUB: no RX2 +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZSP(const s: string): string; +// ZZSP: split mode — STUB: split VFO not yet implemented in ewsdr +begin + if Length(s) = 1 then + Result := '' // accepted, ignored — STUB: split VFO requires separate TX freq handling + else if Length(s) = 0 then + Result := '0' + else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZSQ(const s: string): string; +// ZZSQ: squelch — STUB +begin + if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '000' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZSR(const s: string): string; +// ZZSR: spectrum reference level (dBm, signed 4 chars e.g. "-020") +begin + if Length(s) = 4 then Result := '' // STUB: spec ref not settable via CAT + else if Length(s) = 0 then Result := '-020' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZST: string; +// ZZST: tuning step — STUB: returns 0000 (1 Hz step) +begin Result := ''; end; + +function TCATEngine.ZZSU: string; +// ZZSU: sub-RX update — STUB +begin Result := ''; end; + +function TCATEngine.ZZTA(const s: string): string; +// ZZTA: CTCSS on/off — STUB: FM CTCSS not implemented +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZTB(const s: string): string; +// ZZTB: CTCSS tone frequency index — STUB +begin + if Length(s) = 2 then Result := '' else if Length(s) = 0 then Result := '01' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZTX(const s: string): string; +// ZZTX: TX on/off (0=RX, 1=TX, 2=TUNE) +begin + if Length(s) = 1 then begin + if Assigned(FCtx.SetTransmitting) then FCtx.SetTransmitting(s <> '0'); + Result := ''; + end else if Length(s) = 0 then + Result := B2C(SafeGetTX) + else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZUP(const s: string): string; +// ZZUP: xPA on/off — STUB: no external PA control +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZUS: string; +// ZZUS: two-tone test — STUB: TX DSP not implemented +begin Result := ''; end; + +function TCATEngine.ZZUT(const s: string): string; +// ZZUT: TX update — STUB +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZUX(const s: string): string; +// ZZUX: VFO A lock — STUB +begin + if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' + else Result := CAT_ERROR; +end; + +function TCATEngine.ZZUY(const s: string): string; +// ZZUY: VFO B lock — STUB +begin Result := ZZUX(s); end; + +function TCATEngine.ZZVA(const s: string): string; +// ZZVA: swap VFO A↔B +var tmp: Double; +begin + if Length(s) = 0 then begin + if Assigned(FCtx.GetVfoA) and Assigned(FCtx.GetVfoB) and + Assigned(FCtx.SetVfoA) and Assigned(FCtx.SetVfoB) then begin + tmp := FCtx.GetVfoA; + FCtx.SetVfoA(FCtx.GetVfoB); + FCtx.SetVfoB(tmp); + end; + Result := ''; + end else Result := CAT_ERROR; +end; + +function TCATEngine.ZZVB(const s: string): string; +// ZZVB: copy VFO A to B — STUB: needs explicit operation +begin + if Length(s) = 0 then begin + if Assigned(FCtx.GetVfoA) and Assigned(FCtx.SetVfoB) then + FCtx.SetVfoB(FCtx.GetVfoA); + Result := ''; + end else Result := CAT_ERROR; +end; + +function TCATEngine.ZZVG(const s: string): string; +// ZZVG: volume/gain — alias ZZAG +begin Result := ZZAG(s); end; + +function TCATEngine.ZZVN: string; +// ZZVN: software version string +begin Result := 'EWSDR 0.1.0'; end; + +function TCATEngine.ZZVS(const s: string): string; +// ZZVS: VFO split — STUB +begin Result := ZZSP(s); end; + +// WA..WS: Waterfall settings — STUB: waterfall not controllable via CAT +// These are Thetis-specific waterfall display controls +function TCATEngine.ZZWA(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWB(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWC(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWD(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWE(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWF(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWG(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWH(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWJ(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWK(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWL(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWM(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWN(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWO(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWP(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWQ(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWR(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZWS(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; + +// XA, XC, XF: extended controls — STUB: Thetis-specific hardware +function TCATEngine.ZZXA(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZXC: string; +begin Result := '0'; end; +function TCATEngine.ZZXF(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; + +// YA..YR: Y-axis / spectrum controls — STUB +function TCATEngine.ZZYA(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZYB(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZYC(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZYR(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; + +// ZA..ZZ: System commands — STUB (most are Thetis-specific internal controls) +function TCATEngine.ZZZA(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZZB: string; begin Result := ''; end; +function TCATEngine.ZZZM(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZZP(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; 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; +begin if Length(s) > 0 then Result := '' else Result := '0'; 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; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZZV(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZZW(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; +function TCATEngine.ZZZZ: string; +// ZZZZ: system reset — STUB: would require restarting DSP engine +begin Result := ''; end; +function TCATEngine.ZZZO(const s: string): string; +begin if Length(s) > 0 then Result := '' else Result := '0'; end; + +end. diff --git a/CATSerial.pas b/CATSerial.pas new file mode 100644 index 0000000..0e5b9b0 --- /dev/null +++ b/CATSerial.pas @@ -0,0 +1,275 @@ +unit CATSerial; +{ + CATSerial.pas — CAT через последовательные порты (до 4 штук). + + Каждый порт запускает отдельный поток-слушатель, который: + 1. Читает байты из COM/ttyS порта + 2. Накапливает их в буфер до символа ';' + 3. Передаёт команду в TCATEngine.Parse() + 4. Отправляет ответ обратно в порт + + Платформы: + • Linux : /dev/ttyS0..3, /dev/ttyUSB0, etc. + • Windows: COM1..COM4 (имя порта вводится вручную) + + Зависимости: CATEngine, Serial (FPC fcl-serial RTL unit) +} +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +interface + +uses + Classes, SysUtils, SyncObjs, + Serial, // FPC cross-platform serial unit + {$IFDEF MSWINDOWS}Windows,{$ENDIF} + CATEngine; + +const + CAT_SERIAL_MAX_PORTS = 4; + CAT_SERIAL_BUF_SIZE = 256; + +type + TCATSerialParity = (cspNone, cspOdd, cspEven); + + TCATSerialConfig = record + Enabled: Boolean; + PortName: string; // e.g. '/dev/ttyS0' or 'COM1' + BaudRate: Integer; // 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200 + DataBits: Integer; // 7 or 8 + StopBits: Integer; // 1 or 2 + Parity: TCATSerialParity; + end; + + TCATSerialPort = class; + + { TCATSerialThread — per-port listener thread } + TCATSerialThread = class(TThread) + private + FOwner: TCATSerialPort; + FHandle: TSerialHandle; + FBuf: string; + procedure ProcessBuffer; + function TryOpenPort: Boolean; + protected + procedure Execute; override; + public + constructor Create(AOwner: TCATSerialPort); + end; + + { TCATSerialPort — one serial CAT port } + TCATSerialPort = class + private + FConfig: TCATSerialConfig; + FEngine: TCATEngine; + FThread: TCATSerialThread; + FHandle: TSerialHandle; + FLock: TCriticalSection; + FActive: Boolean; + public + constructor Create(AEngine: TCATEngine; const ACfg: TCATSerialConfig); + destructor Destroy; override; + procedure Start; + procedure Stop; + procedure SendStr(const S: string); + property Handle: TSerialHandle read FHandle write FHandle; + property Config: TCATSerialConfig read FConfig; + property Engine: TCATEngine read FEngine; + property Active: Boolean read FActive; + end; + + { TCATSerialManager — manages up to 4 serial ports } + TCATSerialManager = class + private + FPorts: array[0..CAT_SERIAL_MAX_PORTS-1] of TCATSerialPort; + FEngine: TCATEngine; + public + constructor Create(AEngine: TCATEngine); + destructor Destroy; override; + procedure ApplyConfig(const Configs: array of TCATSerialConfig); + procedure StopAll; + function ActiveCount: Integer; + end; + +implementation + +{ ── TCATSerialThread ──────────────────────────────────────────────────────── } + +constructor TCATSerialThread.Create(AOwner: TCATSerialPort); +begin + FOwner := AOwner; + FBuf := ''; + FreeOnTerminate := False; + inherited Create(True); +end; + +function TCATSerialThread.TryOpenPort: Boolean; +var + cfg: TCATSerialConfig; + h: TSerialHandle; + par: TParityType; +begin + Result := False; + cfg := FOwner.Config; + h := SerOpen(cfg.PortName); + {$IFDEF MSWINDOWS} + if h = TSerialHandle(INVALID_HANDLE_VALUE) then Exit; + {$ELSE} + if h < 0 then Exit; + {$ENDIF} + + case cfg.Parity of + cspNone: par := NoneParity; + cspOdd: par := OddParity; + cspEven: par := EvenParity; + else par := NoneParity; + end; + + SerSetParams(h, cfg.BaudRate, cfg.DataBits, par, cfg.StopBits, []); + FHandle := h; + FOwner.Handle := h; + Result := True; +end; + +procedure TCATSerialThread.ProcessBuffer; +var + tpos: Integer; + cmd, resp: string; +begin + repeat + tpos := Pos(';', FBuf); + if tpos = 0 then Break; + cmd := Copy(FBuf, 1, tpos); + Delete(FBuf, 1, tpos); + resp := FOwner.Engine.Parse(cmd); + if resp <> '' then + FOwner.SendStr(resp); + until tpos = 0; + // Guard against runaway buffers (no terminator seen in 256+ chars) + if Length(FBuf) > CAT_SERIAL_BUF_SIZE then FBuf := ''; +end; + +procedure TCATSerialThread.Execute; +var + ch: Byte; + n: LongInt; +begin + if not TryOpenPort then Exit; + try + while not Terminated do begin + n := SerReadTimeout(FHandle, ch, 50); + if n = 1 then begin + FBuf := FBuf + Chr(ch); + if ch = Ord(';') then ProcessBuffer; + end; + end; + finally + SerClose(FHandle); + FOwner.Handle := -1; + end; +end; + +{ ── TCATSerialPort ────────────────────────────────────────────────────────── } + +constructor TCATSerialPort.Create(AEngine: TCATEngine; const ACfg: TCATSerialConfig); +begin + inherited Create; + FEngine := AEngine; + FConfig := ACfg; + FHandle := -1; + FLock := TCriticalSection.Create; + FActive := False; +end; + +destructor TCATSerialPort.Destroy; +begin + Stop; + FLock.Free; + inherited; +end; + +procedure TCATSerialPort.Start; +begin + if FActive or not FConfig.Enabled then Exit; + FActive := True; + FThread := TCATSerialThread.Create(Self); + FThread.Start; +end; + +procedure TCATSerialPort.Stop; +begin + if not FActive then Exit; + FActive := False; + if Assigned(FThread) then begin + FThread.Terminate; + FThread.WaitFor; + FreeAndNil(FThread); + end; +end; + +procedure TCATSerialPort.SendStr(const S: string); +var buf: AnsiString; +begin + if FHandle < 0 then Exit; + FLock.Acquire; + try + buf := AnsiString(S); + SerWrite(FHandle, buf[1], Length(buf)); + except + // ignore write errors (port may have been closed) + end; + FLock.Release; +end; + +{ ── TCATSerialManager ─────────────────────────────────────────────────────── } + +constructor TCATSerialManager.Create(AEngine: TCATEngine); +var i: Integer; +begin + inherited Create; + FEngine := AEngine; + for i := 0 to CAT_SERIAL_MAX_PORTS-1 do + FPorts[i] := nil; +end; + +destructor TCATSerialManager.Destroy; +begin + StopAll; + inherited; +end; + +procedure TCATSerialManager.ApplyConfig(const Configs: array of TCATSerialConfig); +var + i: Integer; + cnt: Integer; +begin + StopAll; + cnt := Length(Configs); + if cnt > CAT_SERIAL_MAX_PORTS then cnt := CAT_SERIAL_MAX_PORTS; + for i := 0 to cnt-1 do begin + FPorts[i] := TCATSerialPort.Create(FEngine, Configs[i]); + if Configs[i].Enabled then FPorts[i].Start; + end; +end; + +procedure TCATSerialManager.StopAll; +var i: Integer; +begin + for i := 0 to CAT_SERIAL_MAX_PORTS-1 do begin + if Assigned(FPorts[i]) then begin + FPorts[i].Stop; + FreeAndNil(FPorts[i]); + end; + end; +end; + +function TCATSerialManager.ActiveCount: Integer; +var i: Integer; +begin + Result := 0; + for i := 0 to CAT_SERIAL_MAX_PORTS-1 do + if Assigned(FPorts[i]) and FPorts[i].Active then Inc(Result); +end; + +end. diff --git a/CATTcp.pas b/CATTcp.pas new file mode 100644 index 0000000..d95a8f9 --- /dev/null +++ b/CATTcp.pas @@ -0,0 +1,490 @@ +unit CATTcp; +{ + CATTcp.pas — TCP CAT сервер (аналог TCPIPcatServer.cs из Thetis). + + Архитектура: + • Один listening socket (TCATTcpServer) + • На каждое входящее соединение — отдельный поток (TCATTcpClientThread) + • Команды вида "ZZFA00014250000;" обрабатываются через TCATEngine.Parse() + • Поддержка ZZGA/ZZGR для идентификации клиентов (directed broadcast) + • Welcome-строка при подключении + + Порт по умолчанию: 4992 + + Платформы: Linux / Windows (через WebUtils.SockClose/SockShutdown). +} +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +interface + +uses + Classes, SysUtils, SyncObjs, + WebUtils, // SockClose, SockShutdown, SOCK_INVALID + {$IFDEF MSWINDOWS} + Windows, WinSock2 + {$ELSE} + BaseUnix, Sockets + {$ENDIF}, + CATEngine; + +const + CAT_TCP_DEFAULT_PORT = 4992; + CAT_TCP_BUF_SIZE = 4096; + CAT_TCP_WELCOME = '#EWSDR CAT TCP Server;'; + +type + TCATTcpServer = class; + + { TCATTcpClientThread — один поток на TCP клиента } + TCATTcpClientThread = class(TThread) + private + FServer: TCATTcpServer; + FSocket: TSocket; + FBuf: string; + FClientIds: TStringList; + FIdLock: TCriticalSection; + FMarkDelete: Boolean; + + procedure ProcessBuffer; + procedure SendStr(const S: string); + procedure HandleCmd(const Cmd: string); + procedure AddClientId(const Id: string); + procedure RemoveClientId(const Id: string); + protected + procedure Execute; override; + public + constructor Create(AServer: TCATTcpServer; ASocket: TSocket); + destructor Destroy; override; + procedure SendData(const Msg: string; const IdLimit: TStringList = nil); + function IsMarkedForDelete: Boolean; + procedure Disconnect; + end; + + { TCATTcpServer } + TCATTcpServer = class + private + FEngine: TCATEngine; + FPort: Integer; + FListenSock: TSocket; + FServerThread: TThread; + FPurgeThread: TThread; + FClients: TList; + FClientsLock: TCriticalSection; + FRunning: Boolean; + FSendWelcome: Boolean; + FLastError: string; + + procedure ServerLoop; + procedure PurgeLoop; + function InitListen: Boolean; + public + constructor Create(AEngine: TCATEngine; APort: Integer = CAT_TCP_DEFAULT_PORT); + destructor Destroy; override; + procedure Start; + procedure Stop; + procedure Broadcast(const Msg: string; const IdLimit: TStringList = nil); + function ClientCount: Integer; + property Port: Integer read FPort write FPort; + property SendWelcome: Boolean read FSendWelcome write FSendWelcome; + property Running: Boolean read FRunning; + property LastError: string read FLastError; + end; + +implementation + +{ ── internal thread wrappers ─────────────────────────────────────────────── } + +type + TServerThread = class(TThread) + private FServer: TCATTcpServer; + protected procedure Execute; override; + public constructor Create(S: TCATTcpServer); + end; + TPurgeThread = class(TThread) + private FServer: TCATTcpServer; + protected procedure Execute; override; + public constructor Create(S: TCATTcpServer); + end; + +constructor TServerThread.Create(S: TCATTcpServer); +begin FServer := S; FreeOnTerminate := False; inherited Create(True); end; +procedure TServerThread.Execute; begin FServer.ServerLoop; end; + +constructor TPurgeThread.Create(S: TCATTcpServer); +begin FServer := S; FreeOnTerminate := False; inherited Create(True); end; +procedure TPurgeThread.Execute; begin FServer.PurgeLoop; end; + +{ ── TCATTcpClientThread ───────────────────────────────────────────────────── } + +constructor TCATTcpClientThread.Create(AServer: TCATTcpServer; ASocket: TSocket); +begin + FServer := AServer; + FSocket := ASocket; + FBuf := ''; + FMarkDelete := False; + FClientIds := TStringList.Create; + FIdLock := TCriticalSection.Create; + FreeOnTerminate := False; + inherited Create(True); +end; + +destructor TCATTcpClientThread.Destroy; +begin + FClientIds.Free; + FIdLock.Free; + inherited; +end; + +procedure TCATTcpClientThread.AddClientId(const Id: string); +var lo: string; +begin + lo := LowerCase(Id); + FIdLock.Acquire; + try + if FClientIds.IndexOf(lo) < 0 then FClientIds.Add(lo); + finally FIdLock.Release; end; +end; + +procedure TCATTcpClientThread.RemoveClientId(const Id: string); +var lo: string; idx: Integer; +begin + lo := LowerCase(Id); + FIdLock.Acquire; + try + idx := FClientIds.IndexOf(lo); + if idx >= 0 then FClientIds.Delete(idx); + finally FIdLock.Release; end; +end; + +procedure TCATTcpClientThread.SendStr(const S: string); +var + buf: AnsiString; + n: Integer; +begin + if FSocket = SOCK_INVALID then Exit; + buf := AnsiString(S); + if Length(buf) = 0 then Exit; + {$IFDEF MSWINDOWS} + n := send(FSocket, buf[1], Length(buf), 0); + if n = SOCKET_ERROR then begin + {$ELSE} + n := fpSend(FSocket, @buf[1], Length(buf), 0); + if n <= 0 then begin + {$ENDIF} + FMarkDelete := True; + Terminate; + end; +end; + +procedure TCATTcpClientThread.HandleCmd(const Cmd: string); +const + ZZGA_PFX = 'ZZGA'; + ZZGR_PFX = 'ZZGR'; +var + cmd2, guid, resp: string; +begin + cmd2 := Trim(Cmd); + if cmd2 = '' then Exit; + if (Length(cmd2) >= 4) and (UpperCase(Copy(cmd2, 1, 4)) = ZZGA_PFX) then begin + if Length(cmd2) >= 40 then begin // 4 + 36 + guid := LowerCase(Copy(cmd2, 5, 36)); + AddClientId(guid); + SendStr(ZZGA_PFX + guid + ';'); + end else + SendStr('?;'); + end else if (Length(cmd2) >= 4) and (UpperCase(Copy(cmd2, 1, 4)) = ZZGR_PFX) then begin + if Length(cmd2) >= 40 then begin + guid := LowerCase(Copy(cmd2, 5, 36)); + RemoveClientId(guid); + SendStr(ZZGR_PFX + guid + ';'); + end else + SendStr('?;'); + end else begin + resp := FServer.FEngine.Parse(cmd2); + if resp <> '' then SendStr(resp); + end; +end; + +procedure TCATTcpClientThread.ProcessBuffer; +var tpos: Integer; msg: string; +begin + repeat + tpos := Pos(';', FBuf); + if tpos = 0 then Break; + msg := Copy(FBuf, 1, tpos); + Delete(FBuf, 1, tpos); + HandleCmd(msg); + until tpos = 0; + if Length(FBuf) > 1024 then FBuf := ''; +end; + +procedure TCATTcpClientThread.Execute; +var + rawbuf: array[0..CAT_TCP_BUF_SIZE-1] of Byte; + n: Integer; + chunk: AnsiString; +begin + if FServer.SendWelcome then + SendStr(CAT_TCP_WELCOME); + + while not Terminated do begin + {$IFDEF MSWINDOWS} + n := recv(FSocket, rawbuf[0], CAT_TCP_BUF_SIZE, 0); + if n = SOCKET_ERROR then begin FMarkDelete := True; Break; end; + {$ELSE} + n := fpRecv(FSocket, @rawbuf[0], CAT_TCP_BUF_SIZE, 0); + if n <= 0 then begin FMarkDelete := True; Break; end; + {$ENDIF} + SetLength(chunk, n); + Move(rawbuf[0], chunk[1], n); + FBuf := FBuf + string(chunk); + ProcessBuffer; + end; + + SockShutdown(FSocket); + SockClose(FSocket); + FSocket := SOCK_INVALID; +end; + +procedure TCATTcpClientThread.SendData(const Msg: string; const IdLimit: TStringList); +var + i: Integer; + send: Boolean; +begin + if IdLimit = nil then begin + SendStr(Msg); + Exit; + end; + send := False; + FIdLock.Acquire; + try + for i := 0 to FClientIds.Count-1 do + if IdLimit.IndexOf(FClientIds[i]) >= 0 then begin + send := True; + Break; + end; + finally FIdLock.Release; end; + if send then SendStr(Msg); +end; + +function TCATTcpClientThread.IsMarkedForDelete: Boolean; +begin Result := FMarkDelete; end; + +procedure TCATTcpClientThread.Disconnect; +begin + Terminate; + SockShutdown(FSocket); +end; + +{ ── TCATTcpServer ─────────────────────────────────────────────────────────── } + +constructor TCATTcpServer.Create(AEngine: TCATEngine; APort: Integer); +{$IFDEF MSWINDOWS} +var wsa: TWSAData; +{$ENDIF} +begin + inherited Create; + FEngine := AEngine; + FPort := APort; + FListenSock := SOCK_INVALID; + FClients := TList.Create; + FClientsLock := TCriticalSection.Create; + FRunning := False; + FSendWelcome := True; + FLastError := ''; + {$IFDEF MSWINDOWS} + WSAStartup($0202, wsa); + {$ENDIF} +end; + +destructor TCATTcpServer.Destroy; +begin + Stop; + FClients.Free; + FClientsLock.Free; + {$IFDEF MSWINDOWS} + WSACleanup; + {$ENDIF} + inherited; +end; + +function TCATTcpServer.InitListen: Boolean; +var + Addr: {$IFDEF MSWINDOWS}TSockAddrIn{$ELSE}TInetSockAddr{$ENDIF}; + One: Integer; +begin + Result := False; + {$IFDEF MSWINDOWS} + FListenSock := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + {$ELSE} + FListenSock := fpSocket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + {$ENDIF} + if FListenSock = SOCK_INVALID then begin + FLastError := 'Cannot create socket'; + Exit; + end; + + One := 1; + FillChar(Addr, SizeOf(Addr), 0); + Addr.sin_family := AF_INET; + Addr.sin_port := htons(FPort); + + {$IFDEF MSWINDOWS} + setsockopt(FListenSock, SOL_SOCKET, SO_REUSEADDR, PChar(@One), SizeOf(One)); + Addr.sin_addr.S_addr := INADDR_ANY; + if bind(FListenSock, @Addr, SizeOf(Addr)) = SOCKET_ERROR then begin + FLastError := 'bind failed'; + SockClose(FListenSock); FListenSock := SOCK_INVALID; Exit; + end; + if listen(FListenSock, 8) = SOCKET_ERROR then begin + FLastError := 'listen failed'; + SockClose(FListenSock); FListenSock := SOCK_INVALID; Exit; + end; + {$ELSE} + fpSetSockOpt(FListenSock, SOL_SOCKET, SO_REUSEADDR, @One, SizeOf(One)); + Addr.sin_addr.s_addr := htonl(INADDR_ANY); + if fpBind(FListenSock, @Addr, SizeOf(Addr)) <> 0 then begin + FLastError := 'bind failed on port ' + IntToStr(FPort); + SockClose(FListenSock); FListenSock := SOCK_INVALID; Exit; + end; + if fpListen(FListenSock, 8) <> 0 then begin + FLastError := 'listen failed'; + SockClose(FListenSock); FListenSock := SOCK_INVALID; Exit; + end; + {$ENDIF} + Result := True; +end; + +procedure TCATTcpServer.Start; +begin + if FRunning then Exit; + if not InitListen then Exit; + FRunning := True; + FServerThread := TServerThread.Create(Self); + TServerThread(FServerThread).Start; + FPurgeThread := TPurgeThread.Create(Self); + TPurgeThread(FPurgeThread).Start; +end; + +procedure TCATTcpServer.Stop; +var + i: Integer; + cli: TCATTcpClientThread; +begin + if not FRunning then Exit; + FRunning := False; + + if FListenSock <> SOCK_INVALID then begin + SockShutdown(FListenSock); + SockClose(FListenSock); + FListenSock := SOCK_INVALID; + end; + + if Assigned(FServerThread) then begin + FServerThread.Terminate; + FServerThread.WaitFor; + FreeAndNil(FServerThread); + end; + if Assigned(FPurgeThread) then begin + FPurgeThread.Terminate; + FPurgeThread.WaitFor; + FreeAndNil(FPurgeThread); + end; + + FClientsLock.Acquire; + try + for i := 0 to FClients.Count-1 do begin + cli := TCATTcpClientThread(FClients[i]); + cli.Disconnect; + cli.WaitFor; + cli.Free; + end; + FClients.Clear; + finally FClientsLock.Release; end; +end; + +procedure TCATTcpServer.ServerLoop; +var + cli: TCATTcpClientThread; + clientSock: TSocket; + clientAddr: {$IFDEF MSWINDOWS}TSockAddrIn{$ELSE}TInetSockAddr{$ENDIF}; + addrLen: {$IFDEF MSWINDOWS}Integer{$ELSE}TSockLen{$ENDIF}; +begin + addrLen := SizeOf(clientAddr); + while FRunning do begin + {$IFDEF MSWINDOWS} + clientSock := accept(FListenSock, @clientAddr, @addrLen); + if clientSock = INVALID_SOCKET then Break; + {$ELSE} + clientSock := fpAccept(FListenSock, @clientAddr, @addrLen); + if clientSock = SOCK_INVALID then Break; + {$ENDIF} + if not FRunning then begin SockClose(clientSock); Break; end; + + cli := TCATTcpClientThread.Create(Self, clientSock); + FClientsLock.Acquire; + try FClients.Add(cli); + finally FClientsLock.Release; end; + cli.Start; + end; +end; + +procedure TCATTcpServer.PurgeLoop; +var + i: Integer; + cli: TCATTcpClientThread; + del: TList; +begin + del := TList.Create; + try + while FRunning do begin + Sleep(5000); + del.Clear; + FClientsLock.Acquire; + try + for i := FClients.Count-1 downto 0 do begin + cli := TCATTcpClientThread(FClients[i]); + if cli.IsMarkedForDelete then begin + del.Add(cli); + FClients.Delete(i); + end; + end; + finally FClientsLock.Release; end; + for i := 0 to del.Count-1 do begin + cli := TCATTcpClientThread(del[i]); + cli.WaitFor; + cli.Free; + end; + end; + finally del.Free; end; +end; + +procedure TCATTcpServer.Broadcast(const Msg: string; const IdLimit: TStringList); +var + i: Integer; + cli: TCATTcpClientThread; +begin + FClientsLock.Acquire; + try + for i := 0 to FClients.Count-1 do begin + cli := TCATTcpClientThread(FClients[i]); + if not cli.IsMarkedForDelete then + cli.SendData(Msg, IdLimit); + end; + finally FClientsLock.Release; end; +end; + +function TCATTcpServer.ClientCount: Integer; +var i: Integer; +begin + Result := 0; + FClientsLock.Acquire; + try + for i := 0 to FClients.Count-1 do + if not TCATTcpClientThread(FClients[i]).IsMarkedForDelete then Inc(Result); + finally FClientsLock.Release; end; +end; + +end. diff --git a/MainForm.pas b/MainForm.pas index 1b02590..d07d9dd 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -29,7 +29,8 @@ uses WDSP, WDSPEngine, AudioOutput, AudioInput, IntfGraphics, FPImage, Settings, - WebServer; + WebServer, + CATEngine, CATSerial, CATTcp; const CLR_BG = TColor($00101010); @@ -286,6 +287,12 @@ type // --- Settings --- FSettings: TSettingsManager; FWebServer: TWebServer; // веб-интерфейс (порт 8080) + // --- CAT --- + FCATEngine: TCATEngine; + FCATSerial: TCATSerialManager; + FCATTcp: TCATTcpServer; + FCATSyncFreq: Double; + FCATLastGlobal: TGlobalSettings; // текущие CAT-настройки (для сохранения) // Временные поля для передачи параметров в Synchronize-методы FWebSyncFreq: Double; FWebSyncInt: Integer; @@ -532,6 +539,45 @@ type procedure SyncWebCenter; procedure SyncWebMOX; procedure SyncWebDrive; + // --- CAT callbacks ------------------------------------------------------- + function CATGetVfoA: Double; + function CATGetVfoB: Double; + function CATGetMode: Integer; + function CATGetActiveVfo: Integer; + function CATGetAGCMode: Integer; + function CATGetVolume: Integer; + function CATGetDriveLevel: Integer; + function CATGetFilterIdx: Integer; + function CATGetFilterBW: Integer; + function CATGetNRMode: Integer; + function CATGetNBMode: Integer; + function CATGetSNB: Boolean; + function CATGetANF: Boolean; + function CATGetTX: Boolean; + function CATGetRunning: Boolean; + function CATGetSMeter: Double; + function CATGetBand: Integer; + procedure CATSetVfoA(V: Double); + procedure CATSetVfoB(V: Double); + procedure CATSetFilterIdx(V: Integer); + procedure CATDoBandUp; + procedure CATDoBandDown; + procedure CATDoTuneUp; + procedure CATDoTuneDown; + procedure SyncCATVfoA; + procedure SyncCATVfoB; + procedure SyncCATBandUp; + procedure SyncCATBandDown; + procedure SyncCATTuneUp; + procedure SyncCATTuneDown; + procedure InitCATEngine; + procedure CATApplySettings(const G: TGlobalSettings); + procedure OnCATSettingsChange( + const SerEnabled: array of Boolean; + const SerPort: array of string; + const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; + TcpEnabled: Boolean; TcpPort: Integer); + // ------------------------------------------------------------------------- procedure BtnWfAGCClick(Sender: TObject); procedure BtnWfNFClick(Sender: TObject); procedure BtnHidePanelClick(Sender: TObject); @@ -974,6 +1020,15 @@ begin // Audio device names Result.AudioOutDevice := FAudioOutDevName; Result.AudioInDevice := FAudioInDevName; + // CAT settings — carried from the last loaded/saved device config + Result.CATSerialEnabled := FCATLastGlobal.CATSerialEnabled; + Result.CATSerialPort := FCATLastGlobal.CATSerialPort; + Result.CATSerialBaud := FCATLastGlobal.CATSerialBaud; + Result.CATSerialDataBits := FCATLastGlobal.CATSerialDataBits; + Result.CATSerialStopBits := FCATLastGlobal.CATSerialStopBits; + Result.CATSerialParity := FCATLastGlobal.CATSerialParity; + Result.CATTcpEnabled := FCATLastGlobal.CATTcpEnabled; + Result.CATTcpPort := FCATLastGlobal.CATTcpPort; end; procedure TMainForm.SaveCurrentBand; @@ -1159,6 +1214,8 @@ begin FWebServer.OnMOX := WebOnMOX; FWebServer.OnDrive := WebOnDrive; FWebServer.Start; + FillChar(FCATLastGlobal, SizeOf(FCATLastGlobal), 0); + InitCATEngine; FSMeterPeak := -130; FSMeterMin := -130; FSMeterAvg := -130; @@ -1298,6 +1355,9 @@ begin FSettings.Free; FWebServer.Stop; FWebServer.Free; + if Assigned(FCATTcp) then begin FCATTcp.Stop; FreeAndNil(FCATTcp); end; + if Assigned(FCATSerial) then begin FCATSerial.StopAll; FreeAndNil(FCATSerial); end; + FreeAndNil(FCATEngine); end; procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction); @@ -3909,6 +3969,8 @@ begin Move(FNetwork.Device.MAC[0], FDevMAC[0], 6); FDevConnected := True; FSettings.LoadDevice(FDevMAC, G_Settings, FBandCache); + FCATLastGlobal := G_Settings; + CATApplySettings(G_Settings); FVolume := G_Settings.Volume; FActiveVfo := G_Settings.ActiveVfo; // PA settings @@ -5790,9 +5852,19 @@ begin SF.OnVisibilityChange := ApplyVisibility; SF.OnFPSChange := ApplyFPS; SF.OnPAChange := OnPASettingsChange; + SF.OnCATChange := OnCATSettingsChange; end; SF := TSettingsForm(FSettingsForm); SF.LoadPASettings(FPAMaxPower, FPABandCal); + SF.LoadCATSettings( + FCATLastGlobal.CATSerialEnabled, + FCATLastGlobal.CATSerialPort, + FCATLastGlobal.CATSerialBaud, + FCATLastGlobal.CATSerialDataBits, + FCATLastGlobal.CATSerialStopBits, + FCATLastGlobal.CATSerialParity, + FCATLastGlobal.CATTcpEnabled, + FCATLastGlobal.CATTcpPort); // Перечисляем PA устройства SF.RefreshAudioDevices(FAudioOut, FAudioIn); SF.LoadVisibility(FShowSpectrum, FShowWaterfall); @@ -5841,5 +5913,240 @@ begin EnsureWDSPWisdom; end; +// =========================================================================== +// CAT subsystem +// =========================================================================== + +procedure TMainForm.InitCATEngine; +var + Ctx: TCATContext; +begin + FillChar(Ctx, SizeOf(Ctx), 0); + Ctx.GetVfoA := CATGetVfoA; + Ctx.GetVfoB := CATGetVfoB; + Ctx.GetMode := CATGetMode; + Ctx.GetActiveVfo := CATGetActiveVfo; + Ctx.GetAGCMode := CATGetAGCMode; + Ctx.GetVolume := CATGetVolume; + Ctx.GetDriveLevel := CATGetDriveLevel; + Ctx.GetFilterIdx := CATGetFilterIdx; + Ctx.GetFilterBW := CATGetFilterBW; + Ctx.GetNRMode := CATGetNRMode; + Ctx.GetNBMode := CATGetNBMode; + Ctx.GetSNBEnabled := CATGetSNB; + Ctx.GetANFEnabled := CATGetANF; + Ctx.GetTransmitting := CATGetTX; + Ctx.GetRunning := CATGetRunning; + Ctx.GetSMeter := CATGetSMeter; + Ctx.GetCurrentBand := CATGetBand; + Ctx.SetVfoA := CATSetVfoA; + Ctx.SetVfoB := CATSetVfoB; + Ctx.SetMode := WebOnMode; + Ctx.SetActiveVfo := WebOnActiveVfo; + Ctx.SetAGCMode := WebOnAGC; + Ctx.SetVolume := WebOnVolume; + Ctx.SetDriveLevel := WebOnDrive; + Ctx.SetFilterIdx := CATSetFilterIdx; + Ctx.SetNRMode := WebOnNR; + Ctx.SetNBMode := WebOnNB; + Ctx.SetSNBEnabled := WebOnSNB; + Ctx.SetANFEnabled := WebOnANF; + Ctx.SetTransmitting := WebOnMOX; + Ctx.DoBandUp := CATDoBandUp; + Ctx.DoBandDown := CATDoBandDown; + Ctx.DoTuneUp := CATDoTuneUp; + Ctx.DoTuneDown := CATDoTuneDown; + Ctx.DoBandByIndex := WebOnBand; + FCATEngine := TCATEngine.Create(Ctx); + FCATSerial := TCATSerialManager.Create(FCATEngine); + FCATTcp := TCATTcpServer.Create(FCATEngine); +end; + +procedure TMainForm.CATApplySettings(const G: TGlobalSettings); +var + Cfgs: array[0..3] of TCATSerialConfig; + i: Integer; +begin + for i := 0 to 3 do + begin + Cfgs[i].Enabled := G.CATSerialEnabled[i]; + Cfgs[i].PortName := G.CATSerialPort[i]; + Cfgs[i].BaudRate := G.CATSerialBaud[i]; + Cfgs[i].DataBits := G.CATSerialDataBits[i]; + Cfgs[i].StopBits := G.CATSerialStopBits[i]; + case G.CATSerialParity[i] of + 1: Cfgs[i].Parity := cspOdd; + 2: Cfgs[i].Parity := cspEven; + else Cfgs[i].Parity := cspNone; + end; + end; + FCATSerial.ApplyConfig(Cfgs); + FCATTcp.Stop; + if G.CATTcpEnabled then + begin + FCATTcp.Port := G.CATTcpPort; + FCATTcp.Start; + end; +end; + +procedure TMainForm.OnCATSettingsChange( + const SerEnabled: array of Boolean; + const SerPort: array of string; + const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; + TcpEnabled: Boolean; TcpPort: Integer); +var i: Integer; +begin + for i := 0 to 3 do + begin + if i <= High(SerEnabled) then FCATLastGlobal.CATSerialEnabled[i] := SerEnabled[i]; + if i <= High(SerPort) then FCATLastGlobal.CATSerialPort[i] := SerPort[i]; + if i <= High(SerBaud) then FCATLastGlobal.CATSerialBaud[i] := SerBaud[i]; + if i <= High(SerDataBits) then FCATLastGlobal.CATSerialDataBits[i] := SerDataBits[i]; + if i <= High(SerStopBits) then FCATLastGlobal.CATSerialStopBits[i] := SerStopBits[i]; + if i <= High(SerParity) then FCATLastGlobal.CATSerialParity[i] := SerParity[i]; + end; + FCATLastGlobal.CATTcpEnabled := TcpEnabled; + FCATLastGlobal.CATTcpPort := TcpPort; + CATApplySettings(FCATLastGlobal); + if FDevConnected then begin + FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings); + FSettings.Save; + end; +end; + +// --- Getters (called from CAT thread — read only, no sync needed) ----------- + +function TMainForm.CATGetVfoA: Double; begin Result := FVfoA; end; +function TMainForm.CATGetVfoB: Double; begin Result := FVfoB; end; +function TMainForm.CATGetMode: Integer; begin Result := FMode; end; +function TMainForm.CATGetActiveVfo: Integer; begin Result := FActiveVfo; end; +function TMainForm.CATGetAGCMode: Integer; begin Result := FAGCMode; end; +function TMainForm.CATGetVolume: Integer; begin Result := FVolume; end; +function TMainForm.CATGetDriveLevel: Integer;begin Result := TrkDrive.Position; end; +function TMainForm.CATGetFilterIdx: Integer; begin Result := FFilter; end; +function TMainForm.CATGetFilterBW: Integer; begin Result := FFilterBW; end; +function TMainForm.CATGetNRMode: Integer; begin Result := BtnNR.Tag; end; +function TMainForm.CATGetNBMode: Integer; begin Result := BtnNB.Tag; end; +function TMainForm.CATGetSNB: Boolean; begin Result := BtnSNB.Tag <> 0; end; +function TMainForm.CATGetANF: Boolean; begin Result := BtnANF.Tag <> 0; end; +function TMainForm.CATGetTX: Boolean; begin Result := FTransmitting; end; +function TMainForm.CATGetRunning: Boolean; begin Result := FRunning; end; +function TMainForm.CATGetSMeter: Double; begin Result := FLastSMeter; end; +function TMainForm.CATGetBand: Integer; begin Result := FCurrentBand; end; + +// --- Setters ---------------------------------------------------------------- + +procedure TMainForm.CATSetVfoA(V: Double); +begin + FCATSyncFreq := V; + TThread.Synchronize(nil, SyncCATVfoA); +end; + +procedure TMainForm.CATSetVfoB(V: Double); +begin + FCATSyncFreq := V; + TThread.Synchronize(nil, SyncCATVfoB); +end; + +procedure TMainForm.CATSetFilterIdx(V: Integer); +begin + // WebOnFilter: negative value encodes 0-based index as -(idx+1) + WebOnFilter(-(V + 1)); +end; + +procedure TMainForm.CATDoBandUp; +begin TThread.Synchronize(nil, SyncCATBandUp); end; + +procedure TMainForm.CATDoBandDown; +begin TThread.Synchronize(nil, SyncCATBandDown); end; + +procedure TMainForm.CATDoTuneUp; +begin TThread.Synchronize(nil, SyncCATTuneUp); end; + +procedure TMainForm.CATDoTuneDown; +begin TThread.Synchronize(nil, SyncCATTuneDown); end; + +// --- Sync methods (run in main thread) -------------------------------------- + +procedure TMainForm.SyncCATVfoA; +var BandIdx: Integer; +begin + FVfoA := FCATSyncFreq; + if FActiveVfo = 0 then + begin + ApplyVfoA(Round(FVfoA)); + end + else + begin + // VFO A is not active — just update display and band indicator + FreqDispA.Frequency := Round(FVfoA); + BandIdx := FreqToBandIdx(FVfoA); + if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then + begin + StyleButton(BtnBand[FCurrentBand], False); + FCurrentBand := BandIdx; + StyleButton(BtnBand[FCurrentBand], True); + end; + end; +end; + +procedure TMainForm.SyncCATVfoB; +var BandIdx: Integer; +begin + FVfoB := FCATSyncFreq; + FreqDispB.Frequency := Round(FVfoB); + if FActiveVfo = 1 then + begin + FCenterFreq := FVfoB; + if FWDSPReady then FDSPEngine.SetShift(0.0); + if FRunning then + FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel); + BandIdx := FreqToBandIdx(FVfoB); + if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then + begin + StyleButton(BtnBand[FCurrentBand], False); + FCurrentBand := BandIdx; + StyleButton(BtnBand[FCurrentBand], True); + end; + FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; + DrawSpectrum; PbSpectrum.Invalidate; + if PbRuler <> nil then PbRuler.Invalidate; + UpdateVfoDisplay; + end; +end; + +procedure TMainForm.SyncCATBandUp; +begin + if FCurrentBand < BAND_COUNT - 1 then + WebOnBand(FCurrentBand + 1); +end; + +procedure TMainForm.SyncCATBandDown; +begin + if FCurrentBand > 0 then + WebOnBand(FCurrentBand - 1); +end; + +procedure TMainForm.SyncCATTuneUp; +begin + if FActiveVfo = 0 then + ApplyVfoA(Round(FVfoA) + 10) + else + begin + FCATSyncFreq := FVfoB + 10; + SyncCATVfoB; + end; +end; + +procedure TMainForm.SyncCATTuneDown; +begin + if FActiveVfo = 0 then + ApplyVfoA(Round(FVfoA) - 10) + else + begin + FCATSyncFreq := FVfoB - 10; + SyncCATVfoB; + end; +end; end. diff --git a/Settings.pas b/Settings.pas index 62e8b61..69d1525 100644 --- a/Settings.pas +++ b/Settings.pas @@ -75,6 +75,17 @@ type // --- PA (Power Amplifier) settings --- PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200) PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0 + // --- CAT (Computer Aided Transceiver) settings --- + // До 4 последовательных портов (Linux: /dev/ttyS0, Windows: COM1 и т.д.) + CATSerialEnabled: array[0..3] of Boolean; + CATSerialPort: array[0..3] of string; // имя порта ОС + CATSerialBaud: array[0..3] of Integer; // скорость: 1200..115200 + CATSerialDataBits: array[0..3] of Integer; // 7 или 8 + CATSerialStopBits: array[0..3] of Integer; // 1 или 2 + CATSerialParity: array[0..3] of Integer; // 0=None, 1=Odd, 2=Even + // TCP CAT сервер (один, многоклиентский) + CATTcpEnabled: Boolean; + CATTcpPort: Integer; // порт, по умолчанию 4992 end; TSettingsManager = class @@ -191,6 +202,21 @@ begin G.ShowSpectrum := True; G.ShowWaterfall := True; G.DisplayFPS := 60; + // CAT defaults + for i := 0 to 3 do begin + G.CATSerialEnabled[i] := False; + {$IFDEF MSWINDOWS} + G.CATSerialPort[i] := 'COM' + IntToStr(i+1); + {$ELSE} + G.CATSerialPort[i] := '/dev/ttyS' + IntToStr(i); + {$ENDIF} + G.CATSerialBaud[i] := 9600; + G.CATSerialDataBits[i] := 8; + G.CATSerialStopBits[i] := 1; + G.CATSerialParity[i] := 0; // None + end; + G.CATTcpEnabled := False; + G.CATTcpPort := 4992; end; constructor TSettingsManager.Create(const FilePath: string); @@ -388,6 +414,21 @@ begin G.PAMaxPower := JD(GObj,'pa_max_power',100.0); for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := JD(GObj,'pa_band_cal_'+IntToStr(i),100.0); + // CAT serial settings + for i := 0 to 3 do begin + G.CATSerialEnabled[i] := JB(GObj,'cat_serial_en_'+IntToStr(i), False); + {$IFDEF MSWINDOWS} + G.CATSerialPort[i] := JS(GObj,'cat_serial_port_'+IntToStr(i), 'COM'+IntToStr(i+1)); + {$ELSE} + G.CATSerialPort[i] := JS(GObj,'cat_serial_port_'+IntToStr(i), '/dev/ttyS'+IntToStr(i)); + {$ENDIF} + G.CATSerialBaud[i] := JI(GObj,'cat_serial_baud_'+IntToStr(i), 9600); + G.CATSerialDataBits[i] := JI(GObj,'cat_serial_dbits_'+IntToStr(i), 8); + G.CATSerialStopBits[i] := JI(GObj,'cat_serial_sbits_'+IntToStr(i), 1); + G.CATSerialParity[i] := JI(GObj,'cat_serial_parity_'+IntToStr(i), 0); + end; + G.CATTcpEnabled := JB(GObj,'cat_tcp_enabled', False); + G.CATTcpPort := JI(GObj,'cat_tcp_port', 4992); for i := 0 to CFG_BAND_COUNT-1 do begin @@ -449,6 +490,17 @@ begin JW(O,'pa_max_power',G.PAMaxPower); for i := 0 to CFG_BAND_COUNT-1 do JW(O,'pa_band_cal_'+IntToStr(i),G.PABandCal[i]); + // CAT serial settings + for i := 0 to 3 do begin + JW(O,'cat_serial_en_'+IntToStr(i), G.CATSerialEnabled[i]); + JWS(O,'cat_serial_port_'+IntToStr(i),G.CATSerialPort[i]); + JW(O,'cat_serial_baud_'+IntToStr(i), G.CATSerialBaud[i]); + JW(O,'cat_serial_dbits_'+IntToStr(i),G.CATSerialDataBits[i]); + JW(O,'cat_serial_sbits_'+IntToStr(i),G.CATSerialStopBits[i]); + JW(O,'cat_serial_parity_'+IntToStr(i),G.CATSerialParity[i]); + end; + JW(O,'cat_tcp_enabled',G.CATTcpEnabled); + JW(O,'cat_tcp_port',G.CATTcpPort); end; procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer; diff --git a/SettingsForm.pas b/SettingsForm.pas index 1bc1814..ac11f71 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -43,6 +43,14 @@ type TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object; TOnVisibilityChange = procedure(ShowSpectrum, ShowWaterfall: Boolean) of object; TOnFPSChange = procedure(FPS: Integer) of object; + TOnCATChange = procedure( + const SerEnabled: array of Boolean; + const SerPort: array of string; + const SerBaud: array of Integer; + const SerDataBits: array of Integer; + const SerStopBits: array of Integer; + const SerParity: array of Integer; + TcpEnabled: Boolean; TcpPort: Integer) of object; { TSettingsForm } TSettingsForm = class(TForm) @@ -56,12 +64,14 @@ type FPageWaterfall: TScrollBox; FPagePA: TScrollBox; FPageAdvanced: TScrollBox; + FPageCAT: TScrollBox; FNavAudio: TFlatButton; FNavDisplay: TFlatButton; FNavSpectrum: TFlatButton; FNavWaterfall: TFlatButton; FNavPA: TFlatButton; FNavAdvanced: TFlatButton; + FNavCAT: TFlatButton; // ---- Audio tab controls ---- FCmbRXDev: TComboBox; @@ -96,10 +106,21 @@ type FEdMaxPower: TSpinEdit; FEdBandCal: array[0..10] of TFloatSpinEdit; + // ---- CAT tab controls ---- + FCATSerialEn: array[0..3] of TCheckBox; + FCATSerialPort: array[0..3] of TEdit; + FCATSerialBaud: array[0..3] of TComboBox; + FCATSerialDataBits: array[0..3] of TComboBox; + FCATSerialStopBits: array[0..3] of TComboBox; + FCATSerialParity: array[0..3] of TComboBox; + FCATTcpEn: TCheckBox; + FCATTcpPort: TSpinEdit; + // ---- Close button ---- FBtnClose: TFlatButton; // ---- Callbacks ---- + FOnCATChange: TOnCATChange; FOnPAChange: TOnPASettingsChange; FOnDisplayChange: TOnDisplayParamChange; FOnWaterfallChange: TOnWaterfallParamChange; @@ -116,6 +137,7 @@ type procedure BuildDisplayTab; procedure BuildRX1Tab; procedure BuildPATab; + procedure BuildCATTab; procedure ApplyTheme; procedure NavClick(Sender: TObject); procedure SelectPage(APage: TWinControl; ANav: TFlatButton); @@ -147,6 +169,9 @@ type procedure OnFPSCmbChange(Sender: TObject); procedure OnMaxPowerChange(Sender: TObject); procedure OnBandCalChange(Sender: TObject); + procedure OnCATSerialChange(Sender: TObject); + procedure OnCATTcpChange(Sender: TObject); + procedure FireCATChange; procedure BtnCloseClick(Sender: TObject); procedure FireDisplayChange; @@ -179,7 +204,13 @@ type procedure LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean); procedure LoadFPS(FPS: Integer); procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double); + procedure LoadCATSettings( + const SerEnabled: array of Boolean; + const SerPort: array of string; + const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; + TcpEnabled: Boolean; TcpPort: Integer); + property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange; property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange; property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange; property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange; @@ -209,6 +240,9 @@ const BTN_H = 24; GRP_TITLE_H = 22; // высота заголовка в group-панелях (title label + divider) + CAT_BAUD_VALS: array[0..7] of Integer = (1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200); + CAT_BAUD_NAMES: array[0..7] of string = ('1200','2400','4800','9600','19200','38400','57600','115200'); + FFT_SIZES: array[0..6] of Integer = (4096, 8192, 16384, 32768, 65536, 131072, 262144); FFT_NAMES: array[0..6] of string = ('4096','8192','16384','32768','65536','131072','262144'); @@ -391,6 +425,7 @@ begin FNavWaterfall := MakeNavButton('Waterfall', 182); FNavPA := MakeNavButton('Power Amplifier', 218); FNavAdvanced := MakeNavButton('Advanced', 254); + FNavCAT := MakeNavButton('CAT', 290); FContentPanel := TPanel.Create(Self); FContentPanel.Parent := Self; @@ -404,11 +439,13 @@ begin FPageWaterfall := MakeScrollPage; FPagePA := MakeScrollPage; FPageAdvanced := MakeScrollPage; + FPageCAT := MakeScrollPage; BuildAudioTab; BuildDisplayTab; BuildRX1Tab; BuildPATab; + BuildCATTab; FBtnClose := TFlatButton.Create(Self); FBtnClose.Parent := Self; @@ -435,7 +472,8 @@ begin else if Sender = FNavSpectrum then SelectPage(FPageSpectrum, FNavSpectrum) else if Sender = FNavWaterfall then SelectPage(FPageWaterfall, FNavWaterfall) else if Sender = FNavPA then SelectPage(FPagePA, FNavPA) - else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced); + else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced) + else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT); end; procedure TSettingsForm.SelectPage(APage: TWinControl; ANav: TFlatButton); @@ -446,6 +484,7 @@ begin FPageWaterfall.Visible := APage = FPageWaterfall; FPagePA.Visible := APage = FPagePA; FPageAdvanced.Visible := APage = FPageAdvanced; + FPageCAT.Visible := APage = FPageCAT; FNavAudio.Active := ANav = FNavAudio; FNavDisplay.Active := ANav = FNavDisplay; @@ -453,6 +492,7 @@ begin FNavWaterfall.Active := ANav = FNavWaterfall; FNavPA.Active := ANav = FNavPA; FNavAdvanced.Active := ANav = FNavAdvanced; + FNavCAT.Active := ANav = FNavCAT; end; // --------------------------------------------------------------------------- @@ -859,6 +899,7 @@ begin if Assigned(FPageWaterfall) then FPageWaterfall.Color := CLR_BG; if Assigned(FPagePA) then FPagePA.Color := CLR_BG; if Assigned(FPageAdvanced) then FPageAdvanced.Color := CLR_BG; + if Assigned(FPageCAT) then FPageCAT.Color := CLR_BG; end; // --------------------------------------------------------------------------- @@ -1294,4 +1335,213 @@ begin Close; end; +// --------------------------------------------------------------------------- +// BuildCATTab +// --------------------------------------------------------------------------- + +procedure TSettingsForm.BuildCATTab; +const + MARGIN = 22; + PAD = 18; + LW = 120; + CX = PAD + LW + 8; + GRP_W = 320; + GRP_H = 240; + GRP_GAP = 16; + CMB_W = 140; + EDT_W = 160; + R1 = 42; + STEP = 32; +var + i, col, row, gx, gy, j: Integer; + Grp: TPanel; + Chk: TCheckBox; + Ed: TEdit; + Cmb: TComboBox; + Spin: TSpinEdit; +begin + with TLabel.Create(Self) do + begin + Parent := FPageCAT; + Caption := 'CAT'; + SetBounds(MARGIN, 18, 400, 28); + Font.Name := UI_FONT; Font.Size := 16; + Font.Style := [fsBold]; Font.Color := CLR_TEXT; + end; + with TLabel.Create(Self) do + begin + Parent := FPageCAT; + Caption := 'Computer Aided Transceiver — serial port and TCP server settings.'; + SetBounds(MARGIN, 48, 680, 20); + Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM; + end; + + for i := 0 to 3 do + begin + row := i div 2; + col := i mod 2; + gx := MARGIN + col * (GRP_W + GRP_GAP); + gy := 86 + row * (GRP_H + GRP_GAP); + + Grp := MakeGroupPanel(FPageCAT, 'Serial Port ' + IntToStr(i + 1), gx, gy, GRP_W, GRP_H); + + Chk := TCheckBox.Create(Self); + Chk.Parent := Grp; + Chk.Caption := 'Enable'; + Chk.SetBounds(PAD, R1, GRP_W - PAD * 2, 22); + Chk.Font.Color := CLR_TEXT; + Chk.Font.Name := UI_FONT; Chk.Font.Size := 9; + Chk.Tag := i; + Chk.OnChange := OnCATSerialChange; + FCATSerialEn[i] := Chk; + + MakeLbl(Grp, 'Port:', PAD, R1 + STEP + 6, LW); + Ed := TEdit.Create(Self); + Ed.Parent := Grp; + Ed.SetBounds(CX, R1 + STEP, EDT_W, BTN_H); + Ed.Color := CLR_INPUT; Ed.Font.Color := CLR_INPUT_TEXT; + Ed.Font.Name := UI_FONT; Ed.Font.Size := 9; + Ed.Tag := i; + Ed.OnChange := OnCATSerialChange; + {$IFDEF MSWINDOWS} + Ed.Text := 'COM' + IntToStr(i + 1); + {$ELSE} + Ed.Text := '/dev/ttyS' + IntToStr(i); + {$ENDIF} + FCATSerialPort[i] := Ed; + + MakeLbl(Grp, 'Baud rate:', PAD, R1 + 2 * STEP + 6, LW); + Cmb := MakeCombo(Grp, CX, R1 + 2 * STEP, CMB_W, OnCATSerialChange); + Cmb.Tag := i; + for j := 0 to High(CAT_BAUD_NAMES) do Cmb.Items.Add(CAT_BAUD_NAMES[j]); + Cmb.ItemIndex := 3; // 9600 + FCATSerialBaud[i] := Cmb; + + MakeLbl(Grp, 'Data bits:', PAD, R1 + 3 * STEP + 6, LW); + Cmb := MakeCombo(Grp, CX, R1 + 3 * STEP, CMB_W, OnCATSerialChange); + Cmb.Tag := i; + Cmb.Items.Add('7'); Cmb.Items.Add('8'); + Cmb.ItemIndex := 1; // 8 + FCATSerialDataBits[i] := Cmb; + + MakeLbl(Grp, 'Stop bits:', PAD, R1 + 4 * STEP + 6, LW); + Cmb := MakeCombo(Grp, CX, R1 + 4 * STEP, CMB_W, OnCATSerialChange); + Cmb.Tag := i; + Cmb.Items.Add('1'); Cmb.Items.Add('2'); + Cmb.ItemIndex := 0; // 1 + FCATSerialStopBits[i] := Cmb; + + MakeLbl(Grp, 'Parity:', PAD, R1 + 5 * STEP + 6, LW); + Cmb := MakeCombo(Grp, CX, R1 + 5 * STEP, CMB_W, OnCATSerialChange); + Cmb.Tag := i; + Cmb.Items.Add('None'); Cmb.Items.Add('Odd'); Cmb.Items.Add('Even'); + Cmb.ItemIndex := 0; // None + FCATSerialParity[i] := Cmb; + end; + + // TCP server group below the two rows of serial port panels + gy := 86 + 2 * (GRP_H + GRP_GAP); + Grp := MakeGroupPanel(FPageCAT, 'TCP CAT Server', MARGIN, gy, GRP_W * 2 + GRP_GAP, 116); + + FCATTcpEn := TCheckBox.Create(Self); + FCATTcpEn.Parent := Grp; + FCATTcpEn.Caption := 'Enable TCP server (default port 4992)'; + FCATTcpEn.SetBounds(PAD, R1, 300, 22); + FCATTcpEn.Font.Color := CLR_TEXT; + FCATTcpEn.Font.Name := UI_FONT; FCATTcpEn.Font.Size := 9; + FCATTcpEn.OnChange := OnCATTcpChange; + + MakeLbl(Grp, 'Port:', PAD, R1 + STEP + 6, LW); + Spin := TSpinEdit.Create(Self); + Spin.Parent := Grp; + Spin.SetBounds(CX, R1 + STEP, 100, BTN_H + 2); + Spin.Color := CLR_INPUT; Spin.Font.Color := CLR_INPUT_TEXT; + Spin.Font.Name := UI_FONT; Spin.Font.Size := 9; + Spin.MinValue := 1; Spin.MaxValue := 65535; + Spin.Value := 4992; + Spin.OnChange := OnCATTcpChange; + FCATTcpPort := Spin; +end; + +// --------------------------------------------------------------------------- +// CAT event handlers +// --------------------------------------------------------------------------- + +procedure TSettingsForm.FireCATChange; +var + SerEn: array[0..3] of Boolean; + SerPort: array[0..3] of string; + SerBaud, SerData, SerStop, SerPar: array[0..3] of Integer; + i, Idx: Integer; +begin + if FLoading then Exit; + if not Assigned(FOnCATChange) then Exit; + for i := 0 to 3 do + begin + SerEn[i] := FCATSerialEn[i].Checked; + SerPort[i] := FCATSerialPort[i].Text; + Idx := FCATSerialBaud[i].ItemIndex; + if (Idx >= 0) and (Idx <= High(CAT_BAUD_VALS)) then + SerBaud[i] := CAT_BAUD_VALS[Idx] + else + SerBaud[i] := 9600; + if FCATSerialDataBits[i].ItemIndex = 0 then SerData[i] := 7 else SerData[i] := 8; + if FCATSerialStopBits[i].ItemIndex = 1 then SerStop[i] := 2 else SerStop[i] := 1; + SerPar[i] := FCATSerialParity[i].ItemIndex; + end; + FOnCATChange(SerEn, SerPort, SerBaud, SerData, SerStop, SerPar, + FCATTcpEn.Checked, FCATTcpPort.Value); +end; + +procedure TSettingsForm.OnCATSerialChange(Sender: TObject); +begin FireCATChange; end; + +procedure TSettingsForm.OnCATTcpChange(Sender: TObject); +begin FireCATChange; end; + +// --------------------------------------------------------------------------- +// LoadCATSettings +// --------------------------------------------------------------------------- + +procedure TSettingsForm.LoadCATSettings( + const SerEnabled: array of Boolean; + const SerPort: array of string; + const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; + TcpEnabled: Boolean; TcpPort: Integer); +var + i, j, Idx: Integer; +begin + FLoading := True; + try + for i := 0 to 3 do + begin + if i <= High(SerEnabled) then FCATSerialEn[i].Checked := SerEnabled[i]; + if i <= High(SerPort) then FCATSerialPort[i].Text := SerPort[i]; + if i <= High(SerBaud) then + begin + Idx := 3; + for j := 0 to High(CAT_BAUD_VALS) do + if CAT_BAUD_VALS[j] = SerBaud[i] then begin Idx := j; Break; end; + FCATSerialBaud[i].ItemIndex := Idx; + end; + if i <= High(SerDataBits) then + begin + if SerDataBits[i] = 7 then FCATSerialDataBits[i].ItemIndex := 0 + else FCATSerialDataBits[i].ItemIndex := 1; + end; + if i <= High(SerStopBits) then + begin + if SerStopBits[i] = 2 then FCATSerialStopBits[i].ItemIndex := 1 + else FCATSerialStopBits[i].ItemIndex := 0; + end; + if i <= High(SerParity) then + FCATSerialParity[i].ItemIndex := EnsureRange(SerParity[i], 0, 2); + end; + FCATTcpEn.Checked := TcpEnabled; + FCATTcpPort.Value := EnsureRange(TcpPort, 1, 65535); + finally + FLoading := False; + end; +end; + end. diff --git a/ewsdr.lpi b/ewsdr.lpi index b262ee0..ffb887e 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -139,6 +139,18 @@ + + + + + + + + + + + +