diff --git a/CATAdapter.pas b/CATAdapter.pas index 7d5658f..92e0541 100644 --- a/CATAdapter.pas +++ b/CATAdapter.pas @@ -39,6 +39,10 @@ type FSyncInt: Integer; FSyncBool: Boolean; FSyncStr: string; + // Какое поле TX-настроек правит SyncTXField (значение — в FSyncInt/FSyncBool). + FSyncTXFld: (txfMicGain, txfFiltLow, txfFiltHigh, txfCompOn, txfCompGain, + txfEQOn, txfTUNLevel, txfFMDev); + FSyncEQ: TCATEQGains; // ── Геттеры (CAT-поток, read-only) ── function GetVfoA: Double; @@ -96,6 +100,76 @@ type procedure SyncCWSpeed; procedure SyncCWKeyerMode; procedure SyncCWText; + // ── Телеграф: тон, break-in, сайдтон ── + function GetCWPitch: Integer; + procedure SetCWPitch(V: Integer); + function GetCWBreakIn: Boolean; + procedure SetCWBreakIn(V: Boolean); + function GetCWHangTime: Integer; + procedure SetCWHangTime(V: Integer); + function GetCWMonitor: Boolean; + procedure SetCWMonitor(V: Boolean); + procedure SyncCWPitch; + procedure SyncCWBreakIn; + procedure SyncCWHangTime; + procedure SyncCWMonitor; + // ── TX-тракт (активный TX-профиль + вкладка Transmit) ── + function GetMicGain: Integer; + procedure SetMicGain(V: Integer); + function GetTXFilterLow: Integer; + procedure SetTXFilterLow(V: Integer); + function GetTXFilterHigh: Integer; + procedure SetTXFilterHigh(V: Integer); + function GetCompOn: Boolean; + procedure SetCompOn(V: Boolean); + function GetCompGain: Integer; + procedure SetCompGain(V: Integer); + function GetTXEQOn: Boolean; + procedure SetTXEQOn(V: Boolean); + function GetTXEQ(out Gains: TCATEQGains): Integer; + procedure SetTXEQ(NumBands: Integer; const Gains: TCATEQGains); + function GetTUNLevel: Integer; + procedure SetTUNLevel(V: Integer); + function GetFMDeviation: Integer; + procedure SetFMDeviation(V: Integer); + function GetTXProfile: Integer; + procedure SetTXProfile(V: Integer); + function GetTXProfileCount: Integer; + function GetMonitorOn: Boolean; + procedure SetMonitorOn(V: Boolean); + function GetMonitorLevel: Integer; + procedure SetMonitorLevel(V: Integer); + function GetTuning: Boolean; + procedure SetTuning(V: Boolean); + function GetTwoTone: Boolean; + procedure SetTwoTone(V: Boolean); + function GetPureSignal: Boolean; + procedure SetPureSignal(V: Boolean); + procedure DoPSSingleCal; + // ── Приёмник и сервис ── + function GetMuted: Boolean; + procedure SetMuted(V: Boolean); + function GetAtten: Integer; + procedure SetAtten(V: Integer); + function GetSupplyVolts: Double; + procedure DoVfoSwap; + function GetMemoryCount: Integer; + procedure SendCWMacro(V: Integer); + procedure SyncMuted; + procedure SyncAtten; + procedure SyncVfoSwap; + procedure SyncCWMacro; + // Правка одного поля TX-настроек: копия записи, поле, SetTXSettings. Так же + // это делает вкладка Transmit — она тоже отдаёт запись целиком. + procedure SyncTXField; + procedure SyncTXEQ; + procedure SyncTXProfile; + procedure SyncMonitorOn; + procedure SyncMonitorLevel; + procedure SyncTuning; + procedure SyncTwoTone; + procedure SyncPureSignal; + procedure SyncPSSingleCal; procedure DoBandUp; procedure DoBandDown; procedure DoTuneUp; @@ -234,6 +308,59 @@ begin Ctx.SetCWKeyerMode := @SetCWKeyerMode; Ctx.SendCWText := @SendCWText; Ctx.CWTextBusy := @GetCWTextBusy; + // Телеграф: тон (ZZCL), break-in и его hang-time (ZZCB/ZZCD), сайдтон (ZZCM). + Ctx.GetCWPitch := @GetCWPitch; + Ctx.SetCWPitch := @SetCWPitch; + Ctx.GetCWBreakIn := @GetCWBreakIn; + Ctx.SetCWBreakIn := @SetCWBreakIn; + Ctx.GetCWHangTime := @GetCWHangTime; + Ctx.SetCWHangTime := @SetCWHangTime; + Ctx.GetCWMonitor := @GetCWMonitor; + Ctx.SetCWMonitor := @SetCWMonitor; + // TX-тракт: усиление микрофона, кромки TX-фильтра, компрессор, эквалайзер, + // TUN/2TON/PS и выбор TX-профиля. + Ctx.GetMicGain := @GetMicGain; + Ctx.SetMicGain := @SetMicGain; + Ctx.GetTXFilterLow := @GetTXFilterLow; + Ctx.SetTXFilterLow := @SetTXFilterLow; + Ctx.GetTXFilterHigh := @GetTXFilterHigh; + Ctx.SetTXFilterHigh := @SetTXFilterHigh; + Ctx.GetCompOn := @GetCompOn; + Ctx.SetCompOn := @SetCompOn; + Ctx.GetCompGain := @GetCompGain; + Ctx.SetCompGain := @SetCompGain; + Ctx.GetTXEQOn := @GetTXEQOn; + Ctx.SetTXEQOn := @SetTXEQOn; + Ctx.GetTXEQ := @GetTXEQ; + Ctx.SetTXEQ := @SetTXEQ; + Ctx.GetTUNLevel := @GetTUNLevel; + Ctx.SetTUNLevel := @SetTUNLevel; + Ctx.GetFMDeviation := @GetFMDeviation; + Ctx.SetFMDeviation := @SetFMDeviation; + Ctx.GetTXProfile := @GetTXProfile; + Ctx.SetTXProfile := @SetTXProfile; + Ctx.GetTXProfileCount := @GetTXProfileCount; + Ctx.GetMonitorOn := @GetMonitorOn; + Ctx.SetMonitorOn := @SetMonitorOn; + Ctx.GetMonitorLevel := @GetMonitorLevel; + Ctx.SetMonitorLevel := @SetMonitorLevel; + Ctx.GetTuning := @GetTuning; + Ctx.SetTuning := @SetTuning; + Ctx.GetTwoTone := @GetTwoTone; + Ctx.SetTwoTone := @SetTwoTone; + Ctx.GetPureSignal := @GetPureSignal; + Ctx.SetPureSignal := @SetPureSignal; + Ctx.DoPSSingleCal := @DoPSSingleCal; + // Приёмник и сервис: MUT (ZZMA), шаговый аттенюатор (ZZRX), напряжение + // питания (ZZRV), обмен VFO (ZZVS), счётчик памяти (ZZMV), CW-макрос (ZZKM). + Ctx.GetMuted := @GetMuted; + Ctx.SetMuted := @SetMuted; + Ctx.GetAtten := @GetAtten; + Ctx.SetAtten := @SetAtten; + Ctx.GetSupplyVolts := @GetSupplyVolts; + Ctx.DoVfoSwap := @DoVfoSwap; + Ctx.GetMemoryCount := @GetMemoryCount; + Ctx.SendCWMacro := @SendCWMacro; Ctx.OnPanelVfoStep := @PanelVfoStep; Ctx.OnPanelEncoder := @PanelEncoder; Ctx.OnPanelButton := @PanelButton; @@ -476,6 +603,279 @@ begin FController.CWXSend(FSyncStr); end; function TCATAdapter.GetCWTextBusy: Boolean; begin Result := FController.CWXBusy; end; +function TCATAdapter.GetCWPitch: Integer; +begin Result := FController.CWSettings.Pitch; end; + +procedure TCATAdapter.SetCWPitch(V: Integer); +begin FSyncInt := V; FController.Invoke(@SyncCWPitch); end; + +procedure TCATAdapter.SyncCWPitch; +var C: TCWSettings; +begin + C := FController.CWSettings; + C.Pitch := FSyncInt; + FController.SetCWSettings(C); +end; + +function TCATAdapter.GetCWBreakIn: Boolean; +begin Result := FController.CWSettings.BreakIn; end; + +procedure TCATAdapter.SetCWBreakIn(V: Boolean); +begin FSyncBool := V; FController.Invoke(@SyncCWBreakIn); end; + +procedure TCATAdapter.SyncCWBreakIn; +var C: TCWSettings; +begin + C := FController.CWSettings; + C.BreakIn := FSyncBool; + FController.SetCWSettings(C); +end; + +function TCATAdapter.GetCWHangTime: Integer; +begin Result := FController.CWSettings.HangTimeMS; end; + +procedure TCATAdapter.SetCWHangTime(V: Integer); +begin FSyncInt := V; FController.Invoke(@SyncCWHangTime); end; + +procedure TCATAdapter.SyncCWHangTime; +var C: TCWSettings; +begin + C := FController.CWSettings; + C.HangTimeMS := FSyncInt; + FController.SetCWSettings(C); +end; + +function TCATAdapter.GetCWMonitor: Boolean; +// Сайдтонов два, и они не взаимозаменяемы: аппаратный звучит в наушниках +// трансивера, программный — в звуковой карте PC. ZZCM правит тот, что отвечает +// за текущий источник манипуляции, иначе команда молча промахивалась бы мимо. +begin + if FController.CWLocalSource then Result := FController.CWSettings.SidetoneSW + else Result := FController.CWSettings.SidetoneHW; +end; + +procedure TCATAdapter.SetCWMonitor(V: Boolean); +begin FSyncBool := V; FController.Invoke(@SyncCWMonitor); end; + +procedure TCATAdapter.SyncCWMonitor; +var C: TCWSettings; +begin + C := FController.CWSettings; + if FController.CWLocalSource then C.SidetoneSW := FSyncBool + else C.SidetoneHW := FSyncBool; + FController.SetCWSettings(C); +end; + +// ── TX-тракт ───────────────────────────────────────────────────────────────── +// Геттеры читают живые TX-настройки контроллера (они же — активный профиль), +// сеттеры кладут значение в scratch и правят одно поле в потоке контроллера. + +function TCATAdapter.GetMicGain: Integer; +begin Result := Round(FController.FTXSettings.MicGainDB); end; + +procedure TCATAdapter.SetMicGain(V: Integer); +begin FSyncTXFld := txfMicGain; FSyncInt := V; FController.Invoke(@SyncTXField); end; + +function TCATAdapter.GetTXFilterLow: Integer; +begin Result := FController.FTXSettings.FilterLow; end; + +procedure TCATAdapter.SetTXFilterLow(V: Integer); +begin FSyncTXFld := txfFiltLow; FSyncInt := V; FController.Invoke(@SyncTXField); end; + +function TCATAdapter.GetTXFilterHigh: Integer; +begin Result := FController.FTXSettings.FilterHigh; end; + +procedure TCATAdapter.SetTXFilterHigh(V: Integer); +begin FSyncTXFld := txfFiltHigh; FSyncInt := V; FController.Invoke(@SyncTXField); end; + +function TCATAdapter.GetCompOn: Boolean; +begin Result := FController.FTXSettings.CompressorOn; end; + +procedure TCATAdapter.SetCompOn(V: Boolean); +begin FSyncTXFld := txfCompOn; FSyncBool := V; FController.Invoke(@SyncTXField); end; + +function TCATAdapter.GetCompGain: Integer; +begin Result := Round(FController.FTXSettings.CompressorGain); end; + +procedure TCATAdapter.SetCompGain(V: Integer); +begin FSyncTXFld := txfCompGain; FSyncInt := V; FController.Invoke(@SyncTXField); end; + +function TCATAdapter.GetTXEQOn: Boolean; +begin Result := FController.FTXSettings.EQOn; end; + +procedure TCATAdapter.SetTXEQOn(V: Boolean); +begin FSyncTXFld := txfEQOn; FSyncBool := V; FController.Invoke(@SyncTXField); end; + +function TCATAdapter.GetTXEQ(out Gains: TCATEQGains): Integer; +var i: Integer; +begin + for i := 0 to High(Gains) do Gains[i] := Round(FController.FTXSettings.EQGains[i]); + Result := FController.FTXSettings.EQNumBands; +end; + +procedure TCATAdapter.SetTXEQ(NumBands: Integer; const Gains: TCATEQGains); +begin + FSyncInt := NumBands; + FSyncEQ := Gains; + FController.Invoke(@SyncTXEQ); +end; + +procedure TCATAdapter.SyncTXEQ; +// Частоты полос команда не несёт (их в формате Thetis попросту нет) — трогаем +// только усиления и число полос, сетка остаётся той, что настроена в UI. +var + T: TTXSettings; + i: Integer; +begin + T := FController.FTXSettings; + T.EQNumBands := FSyncInt; + for i := 0 to High(FSyncEQ) do + if i <= FSyncInt then T.EQGains[i] := FSyncEQ[i]; + FController.SetTXSettings(T); +end; + +function TCATAdapter.GetTUNLevel: Integer; +begin Result := FController.FTXSettings.TUNLevel; end; + +procedure TCATAdapter.SetTUNLevel(V: Integer); +begin FSyncTXFld := txfTUNLevel; FSyncInt := V; FController.Invoke(@SyncTXField); end; + +function TCATAdapter.GetFMDeviation: Integer; +begin Result := Round(FController.FTXSettings.FMDeviation); end; + +procedure TCATAdapter.SetFMDeviation(V: Integer); +begin FSyncTXFld := txfFMDev; FSyncInt := V; FController.Invoke(@SyncTXField); end; + +procedure TCATAdapter.SyncTXField; +var T: TTXSettings; +begin + T := FController.FTXSettings; + case FSyncTXFld of + txfMicGain: T.MicGainDB := FSyncInt; + txfFiltLow: T.FilterLow := FSyncInt; + txfFiltHigh: T.FilterHigh := FSyncInt; + txfCompOn: T.CompressorOn := FSyncBool; + txfCompGain: T.CompressorGain := FSyncInt; + txfEQOn: T.EQOn := FSyncBool; + txfTUNLevel: T.TUNLevel := FSyncInt; + txfFMDev: T.FMDeviation := FSyncInt; + end; + FController.SetTXSettings(T); +end; + +function TCATAdapter.GetTXProfile: Integer; +begin Result := FController.ActiveTXProfile; end; + +procedure TCATAdapter.SetTXProfile(V: Integer); +begin FSyncInt := V; FController.Invoke(@SyncTXProfile); end; + +procedure TCATAdapter.SyncTXProfile; +begin FController.SelectTXProfile(FSyncInt); end; + +function TCATAdapter.GetTXProfileCount: Integer; +begin Result := FController.TXProfileCount; end; + +function TCATAdapter.GetMonitorOn: Boolean; +begin Result := not FController.FRxMuteOnTx; end; + +procedure TCATAdapter.SetMonitorOn(V: Boolean); +begin FSyncBool := V; FController.Invoke(@SyncMonitorOn); end; + +procedure TCATAdapter.SyncMonitorOn; +begin FController.SetRxMuteOnTx(not FSyncBool); end; + +function TCATAdapter.GetMonitorLevel: Integer; +begin Result := FController.FTxMonVolume; end; + +procedure TCATAdapter.SetMonitorLevel(V: Integer); +begin FSyncInt := V; FController.Invoke(@SyncMonitorLevel); end; + +procedure TCATAdapter.SyncMonitorLevel; +begin FController.SetTXMonVolume(FSyncInt); end; + +function TCATAdapter.GetTuning: Boolean; +begin Result := FController.FTuning; end; + +procedure TCATAdapter.SetTuning(V: Boolean); +begin FSyncBool := V; FController.Invoke(@SyncTuning); end; + +procedure TCATAdapter.SyncTuning; +begin FController.SetTune(FSyncBool); end; + +function TCATAdapter.GetTwoTone: Boolean; +begin Result := FController.FTwoTone; end; + +procedure TCATAdapter.SetTwoTone(V: Boolean); +begin FSyncBool := V; FController.Invoke(@SyncTwoTone); end; + +procedure TCATAdapter.SyncTwoTone; +begin FController.SetTwoTone(FSyncBool); end; + +function TCATAdapter.GetPureSignal: Boolean; +begin Result := FController.FPSEnabled; end; + +procedure TCATAdapter.SetPureSignal(V: Boolean); +begin FSyncBool := V; FController.Invoke(@SyncPureSignal); end; + +procedure TCATAdapter.SyncPureSignal; +begin FController.SetPureSignal(FSyncBool); end; + +procedure TCATAdapter.DoPSSingleCal; +begin FController.Invoke(@SyncPSSingleCal); end; + +procedure TCATAdapter.SyncPSSingleCal; +begin FController.PSSingleCal; end; + +// ── Приёмник и сервис ──────────────────────────────────────────────────────── + +function TCATAdapter.GetMuted: Boolean; +begin Result := FController.FMuted; end; + +procedure TCATAdapter.SetMuted(V: Boolean); +begin FSyncBool := V; FController.Invoke(@SyncMuted); end; + +procedure TCATAdapter.SyncMuted; +begin FController.SetMute(FSyncBool); end; + +function TCATAdapter.GetAtten: Integer; +begin Result := FController.FAtten; end; + +procedure TCATAdapter.SetAtten(V: Integer); +begin FSyncInt := V; FController.Invoke(@SyncAtten); end; + +procedure TCATAdapter.SyncAtten; +begin FController.SetAtten(FSyncInt); end; + +function TCATAdapter.GetSupplyVolts: Double; +// Телеметрия платы; -1 = данных ещё нет (движок отдаст ноль, как Thetis). +begin Result := FController.FLastSupplyV; end; + +procedure TCATAdapter.DoVfoSwap; +begin FController.Invoke(@SyncVfoSwap); end; + +procedure TCATAdapter.SyncVfoSwap; +begin FController.VfoSwap; end; + +function TCATAdapter.GetMemoryCount: Integer; +begin + if Assigned(FController.FChannelStore) then Result := FController.FChannelStore.Count + else Result := 0; +end; + +procedure TCATAdapter.SendCWMacro(V: Integer); +begin FSyncInt := V; FController.Invoke(@SyncCWMacro); end; + +procedure TCATAdapter.SyncCWMacro; +// Макрос n (1..9) = ячейка памяти сообщений n. Ячеек восемь (F1..F8): девятая +// и пустые просто ничего не передают — очередь дёргать незачем. +var Idx: Integer; +begin + Idx := FSyncInt - 1; + if (Idx < 0) or (Idx >= CW_MSG_COUNT) then Exit; + if FController.CWSettings.Messages[Idx] = '' then Exit; + FController.CWXSend(FController.CWSettings.Messages[Idx]); +end; + procedure TCATAdapter.SyncMOX; begin FController.SetMOX(FSyncBool); end; diff --git a/CATEngine.pas b/CATEngine.pas index b2f2a0b..ea8dce9 100644 --- a/CATEngine.pas +++ b/CATEngine.pas @@ -37,6 +37,20 @@ const // совпадать: контроллер хранит FFMRptDir в этих же кодах). RPT_NONE = 0; RPT_MINUS = 1; RPT_PLUS = 2; + // Границы TX/CW-полей. Совпадают с контролами вкладок Transmit и CW: движок + // клампует сам, потому что по CAT прилетает что угодно, а контроллер эти + // поля принимает записью целиком и уже не знает, кто их правил. + MIC_GAIN_MIN = -20; MIC_GAIN_MAX = 60; // дБ + TX_FILT_LO_MIN = 0; TX_FILT_LO_MAX = 8000; // Гц + TX_FILT_HI_MIN = 100; TX_FILT_HI_MAX = 12000; // Гц + COMP_GAIN_MAX = 20; // дБ + EQ_GAIN_MIN = -20; EQ_GAIN_MAX = 20; // дБ + CW_PITCH_MIN = 200; CW_PITCH_MAX = 1200; // Гц + CW_HANG_MAX = 2000; // мс + CW_WPM_MIN = 5; CW_WPM_MAX = 60; + CW_TEXT_FIELD = 25; // ширина поля текста KY/ZZKY + FM_DEV_NARROW = 2500; FM_DEV_WIDE = 5000; // Гц (ZZFD — радиокнопка) + type TCATGetDoubleFunc = function: Double of object; TCATGetIntFunc = function: Integer of object; @@ -48,6 +62,11 @@ type // Andromeda front panel input TCATPanelEncoderProc = procedure(Enc, Step: Integer) of object; TCATPanelButtonProc = procedure(Btn: Integer; State, LongPress: Boolean) of object; + // TX-эквалайзер (ZZEB): [0] — преамп, 1..10 — полосы, дБ. Формат провода + // (счётчик полос + знаковые тройки) знает движок, состав записи — адаптер. + TCATEQGains = array[0..10] of Integer; + TCATGetEQFunc = function(out Gains: TCATEQGains): Integer of object; // → число полос + TCATSetEQProc = procedure(NumBands: Integer; const Gains: TCATEQGains) of object; TCATContext = record GetVfoA: TCATGetDoubleFunc; @@ -120,6 +139,61 @@ type SetCWKeyerMode: TCATSetIntProc; SendCWText: procedure(const S: string) of object; CWTextBusy: TCATGetBoolFunc; + // Телеграф, продолжение: тон приёма/сайдтона (ZZCL), break-in прошивки + // (ZZCB) и его hang-time (ZZCD), сайдтон активного источника манипуляции + // (ZZCM — у Thetis это «monitor DISABLE», инверсия делается в движке). + GetCWPitch: TCATGetIntFunc; // Гц + SetCWPitch: TCATSetIntProc; + GetCWBreakIn: TCATGetBoolFunc; + SetCWBreakIn: TCATSetBoolProc; + GetCWHangTime: TCATGetIntFunc; // мс + SetCWHangTime: TCATSetIntProc; + GetCWMonitor: TCATGetBoolFunc; + SetCWMonitor: TCATSetBoolProc; + // ── TX-тракт (вкладка Transmit / активный TX-профиль) ── + GetMicGain: TCATGetIntFunc; // дБ (у EWSDR -20..+60) + SetMicGain: TCATSetIntProc; + GetTXFilterLow: TCATGetIntFunc; // Гц + SetTXFilterLow: TCATSetIntProc; + GetTXFilterHigh: TCATGetIntFunc; // Гц + SetTXFilterHigh: TCATSetIntProc; + GetCompOn: TCATGetBoolFunc; // речевой компрессор (WDSP COMP) + SetCompOn: TCATSetBoolProc; + GetCompGain: TCATGetIntFunc; // дБ 0..20 + SetCompGain: TCATSetIntProc; + GetTXEQOn: TCATGetBoolFunc; + SetTXEQOn: TCATSetBoolProc; + GetTXEQ: TCATGetEQFunc; + SetTXEQ: TCATSetEQProc; + GetTUNLevel: TCATGetIntFunc; // мощность настройки, 0..100 % + SetTUNLevel: TCATSetIntProc; + GetFMDeviation: TCATGetIntFunc; // девиация передачи, Гц + SetFMDeviation: TCATSetIntProc; + GetTXProfile: TCATGetIntFunc; // индекс активного профиля + SetTXProfile: TCATSetIntProc; + GetTXProfileCount: TCATGetIntFunc; + // MON: у EWSDR монитор передачи — это self-monitor даунлинка (DUP + RX MUTE + // off), уровень у него свой, отдельный от RX-громкости. + GetMonitorOn: TCATGetBoolFunc; + SetMonitorOn: TCATSetBoolProc; + GetMonitorLevel: TCATGetIntFunc; // 0..100 + SetMonitorLevel: TCATSetIntProc; + GetTuning: TCATGetBoolFunc; // кнопка TUN + SetTuning: TCATSetBoolProc; + GetTwoTone: TCATGetBoolFunc; // 2TON + SetTwoTone: TCATSetBoolProc; + GetPureSignal: TCATGetBoolFunc; // PS-A + SetPureSignal: TCATSetBoolProc; + DoPSSingleCal: TCATProc; // ZZUS — одиночная калибровка PS + // ── Приёмник и сервис ── + GetMuted: TCATGetBoolFunc; // кнопка MUT (ZZMA) + SetMuted: TCATSetBoolProc; + GetAtten: TCATGetIntFunc; // шаговый аттенюатор ADC, дБ (ZZRX) + SetAtten: TCATSetIntProc; + GetSupplyVolts: TCATGetDoubleFunc; // напряжение питания PA (ZZRV); <0 = нет данных + DoVfoSwap: TCATProc; // обмен VFO A↔B (ZZVS) + GetMemoryCount: TCATGetIntFunc; // число записей памяти каналов (ZZMV) + SendCWMacro: TCATSetIntProc; // запустить CW-макрос (сообщение) номер n (ZZKM) // Andromeda front panel input (ZZZD/ZZZU/ZZZE/ZZZP/ZZZS) OnPanelVfoStep: TCATSetIntProc; // знаковое число шагов энкодера VFO OnPanelEncoder: TCATPanelEncoderProc; // номер энкодера (0-based) + знаковый шаг @@ -167,6 +241,27 @@ type function SafeGetFMStep: Integer; function SafeGetAGCTop: Integer; function SafeGetCTun: Boolean; + // TX-тракт / телеграф + function SafeGetCWKeyerMode: Integer; + function SafeGetCWPitch: Integer; + function SafeGetCWHangTime: Integer; + function SafeGetMicGain: Integer; + function SafeGetTXFilterLow: Integer; + function SafeGetTXFilterHigh: Integer; + function SafeGetCompGain: Integer; + function SafeGetTUNLevel: Integer; + function SafeGetFMDeviation: Integer; + function SafeGetTXProfile: Integer; + function SafeGetMonitorLevel: Integer; + function SafeGetBool(const F: TCATGetBoolFunc): Boolean; + // Общий разбор «1 символ 0/1» для двухпозиционных команд: SET дёргает Setter + // (если он есть), GET отдаёт Getter. Пустой Setter = команда только читает. + function OnOff(const s: string; const Getter: TCATGetBoolFunc; + const Setter: TCATSetBoolProc): string; + // То же для числового поля фиксированной ширины с клампом. + function NumField(const s: string; Width, Lo, Hi: Integer; + const Getter: TCATGetIntFunc; + const Setter: TCATSetIntProc): string; function StepIdxToHz(Idx: Integer): Double; // command implementations @@ -279,6 +374,8 @@ type function ZZNT(const s: string): string; function ZZOA(const s: string): string; function ZZOB(const s: string): string; + function ZZOS(const s: string): string; + function ZZOT(const s: string): string; function ZZPC(const s: string): string; function ZZPD: string; function ZZPO(const s: string): string; @@ -355,6 +452,20 @@ type function ZZZW(const s: string): string; function ZZZZ: string; function ZZZO(const s: string): string; + // ── TX-тракт и телеграф (проброшены в контроллер, см. doc/CAT_STATUS.md) ── + function ZZCI(const s: string): string; + function ZZCL(const s: string): string; + function ZZCM(const s: string): string; + function ZZET(const s: string): string; + function ZZLI(const s: string): string; + function ZZPK(const s: string): string; + function ZZPL(const s: string): string; + function ZZTH(const s: string): string; + function ZZTL(const s: string): string; + function ZZTM(const s: string): string; + function ZZTO(const s: string): string; + function ZZTP(const s: string): string; + function ZZTU(const s: string): string; public constructor Create(const Ctx: TCATContext); @@ -370,6 +481,23 @@ implementation function B2C(B: Boolean): string; inline; begin if B then Result := '1' else Result := '0'; end; +function IsParamless(const Pfx: string): Boolean; +// Кенвудовские команды, у которых и SET, и GET идут с пустым суффиксом +// (nsetparms=0 / ngetparms=0 в CATStructs.xml). Любой хвост у них — ошибка +// формата, а не значение. +const + NoArg: array[0..9] of string = ( + 'BD', 'BU', 'DN', 'UP', // диапазон и шаг настройки + 'TX', 'RX', // передача/приём — тут цена ошибки самая высокая + 'QI', 'RC', // быстрая память, сброс RIT + 'ID', 'IF'); // только чтение, аргумента не бывает +var i: Integer; +begin + for i := Low(NoArg) to High(NoArg) do + if Pfx = NoArg[i] then Exit(True); + Result := False; +end; + { ── Helpers ──────────────────────────────────────────────────────────────── } function TCATEngine.Pad(V: Int64; W: Integer): string; @@ -546,6 +674,83 @@ function TCATEngine.SafeGetCTun: Boolean; begin if Assigned(FCtx.GetCTun) then Result := FCtx.GetCTun else Result := False; end; +function TCATEngine.SafeGetCWKeyerMode: Integer; +begin + if Assigned(FCtx.GetCWKeyerMode) then Result := FCtx.GetCWKeyerMode else Result := 0; +end; +function TCATEngine.SafeGetCWPitch: Integer; +begin + if Assigned(FCtx.GetCWPitch) then Result := FCtx.GetCWPitch else Result := 600; +end; +function TCATEngine.SafeGetCWHangTime: Integer; +begin + if Assigned(FCtx.GetCWHangTime) then Result := FCtx.GetCWHangTime else Result := 300; +end; +function TCATEngine.SafeGetMicGain: Integer; +begin + if Assigned(FCtx.GetMicGain) then Result := FCtx.GetMicGain else Result := 10; +end; +function TCATEngine.SafeGetTXFilterLow: Integer; +begin + if Assigned(FCtx.GetTXFilterLow) then Result := FCtx.GetTXFilterLow else Result := 100; +end; +function TCATEngine.SafeGetTXFilterHigh: Integer; +begin + if Assigned(FCtx.GetTXFilterHigh) then Result := FCtx.GetTXFilterHigh else Result := 3100; +end; +function TCATEngine.SafeGetCompGain: Integer; +begin + if Assigned(FCtx.GetCompGain) then Result := FCtx.GetCompGain else Result := 0; +end; +function TCATEngine.SafeGetTUNLevel: Integer; +begin + if Assigned(FCtx.GetTUNLevel) then Result := FCtx.GetTUNLevel else Result := 10; +end; +function TCATEngine.SafeGetFMDeviation: Integer; +begin + if Assigned(FCtx.GetFMDeviation) then Result := FCtx.GetFMDeviation else Result := 5000; +end; +function TCATEngine.SafeGetTXProfile: Integer; +begin + if Assigned(FCtx.GetTXProfile) then Result := FCtx.GetTXProfile else Result := 0; +end; +function TCATEngine.SafeGetMonitorLevel: Integer; +begin + if Assigned(FCtx.GetMonitorLevel) then Result := FCtx.GetMonitorLevel else Result := 0; +end; +function TCATEngine.SafeGetBool(const F: TCATGetBoolFunc): Boolean; +begin + if Assigned(F) then Result := F else Result := False; +end; + +function TCATEngine.OnOff(const s: string; const Getter: TCATGetBoolFunc; + const Setter: TCATSetBoolProc): string; +// Двухпозиционная команда «0/1;». Формы SET и GET у всех одинаковы — +// держим разбор в одном месте, чтобы тела команд остались про смысл. +begin + if Length(s) = 0 then Exit(B2C(SafeGetBool(Getter))); + if (s <> '0') and (s <> '1') then Exit(CAT_ERROR); + if Assigned(Setter) then Setter(s = '1'); + Result := ''; +end; + +function TCATEngine.NumField(const s: string; Width, Lo, Hi: Integer; + const Getter: TCATGetIntFunc; const Setter: TCATSetIntProc): string; +// Числовое поле фиксированной ширины: SET клампует в [Lo..Hi] (как Thetis — +// выход за границы не ошибка, а подрезка), GET дополняет нулями слева. +var v: Integer; +begin + if Length(s) = 0 then + begin + if Assigned(Getter) then v := Getter else v := Lo; + Exit(Pad(EnsureRange(v, Lo, Hi), Width)); + end; + if Length(s) <> Width then Exit(CAT_ERROR); + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + if Assigned(Setter) then Setter(EnsureRange(v, Lo, Hi)); + Result := ''; +end; + function TCATEngine.StepIdxToHz(Idx: Integer): Double; // Шаг настройки по индексу (0..14) — зеркало Thetis Step2Freq (в Гц). const Steps: array[0..14] of Double = ( @@ -601,6 +806,10 @@ begin Result := ParseZZ(ext, sfx); end else begin sfx := Copy(s, 3, tpos - 3); + // Команды без параметров: эталон отбраковывает лишний суффикс в парсере, + // по таблице ширин, ДО вызова команды. Таблицы у нас нет — держим список + // здесь, иначе «TXfoo;» дошёл бы до CmdTX и поднял передачу. + if (sfx <> '') and IsParamless(pfx) then Exit(CAT_ERROR); if pfx = 'AG' then Result := CmdAG(sfx) else if pfx = 'AI' then Result := CmdAI(sfx) else if pfx = 'BD' then begin CmdBD; Result := ''; end @@ -630,9 +839,9 @@ begin 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 = 'RD' then Result := CmdRD(sfx) else if pfx = 'RT' then Result := CmdRT(sfx) - else if pfx = 'RU' then begin CmdRU(sfx); Result := ''; end + else if pfx = 'RU' then Result := CmdRU(sfx) else if pfx = 'RX' then begin CmdRX(sfx); Result := ''; end else if pfx = 'SH' then Result := CmdSH(sfx) else if pfx = 'SL' then Result := CmdSL(sfx) @@ -706,15 +915,9 @@ begin end; function TCATEngine.CmdCT(const s: string): string; -// CT: CTCSS enable (1=on, 0=off) для FM-передачи. +// CT: включение CTCSS — обёртка над ZZTA, как и в эталоне. begin - if Length(s) = 1 then begin - if Assigned(FCtx.SetCTCSSOn) then FCtx.SetCTCSSOn(s = '1'); - Result := ''; - end else if Length(s) = 0 then - Result := B2C(SafeGetCTCSSOn) - else - Result := CAT_ERROR; + Result := ZZTA(s); end; function TCATEngine.CmdDN: string; @@ -804,7 +1007,8 @@ begin end; function TCATEngine.CmdIF: string; -// IF: interface status — 37-byte composite response +// IF: составной статус — 35 символов ответа (как у Thetis; «37 байт» из +// прежнего комментария — это вся посылка вместе с префиксом и ';'). var freq, step, incr, rit, xit, tx, mode, split: string; m: Integer; @@ -854,8 +1058,14 @@ begin else Result := '0'; Exit; end; + // Поле текста фиксированной ширины: 25 символов, добито пробелами справа. + // Длиннее — ошибка формата: очередь передачи не должна расти произвольно, + // иначе один пакет уводит станцию в эфир на неопределённое время. + if Length(s) > CW_TEXT_FIELD then Exit(CAT_ERROR); Txt := s; if (Length(Txt) > 0) and (Txt[1] = ' ') then Delete(Txt, 1, 1); + Txt := TrimRight(Txt); // иначе хвост набивки ушёл бы в эфир паузами + if Txt = '' then Exit(''); if Assigned(FCtx.SendCWText) then FCtx.SendCWText(Txt); Result := ''; end; @@ -876,22 +1086,29 @@ begin end; function TCATEngine.CmdMG(const s: string): string; -// MG: mic gain (3 digits, 0-100) — STUB: TX DSP not yet implemented +// MG: усиление микрофона по-кенвудовски (000..100). Шкала 100:70 к дБ — как в +// Thetis (n/1.43), дальше кламп под диапазон вкладки Transmit. Отрицательные дБ +// в кенвудовскую шкалу не ложатся: на чтении подрезаем к нулю. +var v: Integer; begin - if Length(s) = 3 then - Result := '' - else if Length(s) = 0 then - Result := '050' + if Length(s) = 0 then + Result := Pad(EnsureRange(Round(SafeGetMicGain / 0.7), 0, 100), 3) + else if Length(s) = 3 then + begin + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + if Assigned(FCtx.SetMicGain) then + FCtx.SetMicGain(EnsureRange(Round(EnsureRange(v, 0, 100) / 1.43), + MIC_GAIN_MIN, MIC_GAIN_MAX)); + Result := ''; + end else Result := CAT_ERROR; end; function TCATEngine.CmdMO(const s: string): string; -// MO: monitor — STUB +// MO: монитор передачи — то же, что ZZMO. begin - if Length(s) = 1 then Result := '' - else if Length(s) = 0 then Result := '0' - else Result := CAT_ERROR; + Result := ZZMO(s); end; function TCATEngine.CmdNB(const s: string): string; @@ -920,20 +1137,22 @@ begin end; function TCATEngine.CmdOF(const s: string): string; -// OF: FM repeater offset frequency (9 digits, Hz). +// OF: сдвиг репитера — обёртка над ZZOT, как и в эталоне. begin - if Length(s) = 9 then begin - if Assigned(FCtx.SetFMRptOffset) then FCtx.SetFMRptOffset(StrToIntDef(s, 0)); - Result := ''; - end else if Length(s) = 0 then - Result := Pad(Round(SafeGetFMRptOffset), 9) - else - Result := CAT_ERROR; + Result := ZZOT(s); end; function TCATEngine.CmdOS(const s: string): string; -// OS: repeater offset direction (Kenwood: 0=simplex, 1=plus, 2=minus). -// Внутренние константы: RPT_NONE=0, RPT_MINUS=1, RPT_PLUS=2 → требуется трансляция. +// OS: направление сдвига репитера — обёртка над ZZOS, как и в эталоне. +begin + Result := ZZOS(s); +end; + +function TCATEngine.ZZOS(const s: string): string; +// ZZOS: направление сдвига репитера. Снаружи (и у Thetis) 0=симплекс, 1=вверх, +// 2=вниз; внутри у нас RPT_NONE/MINUS/PLUS — отсюда трансляция. +// ★Мусор эталон молча трактует как симплекс (default в String2OffsetDirection), +// и мы повторяем это намеренно: клиенты рассчитывают на такое поведение. var dir: Integer; begin if Length(s) = 1 then begin @@ -954,6 +1173,23 @@ begin Result := CAT_ERROR; end; +function TCATEngine.ZZOT(const s: string): string; +// ZZOT: величина сдвига репитера, 9 цифр. Эталон читает поле как МГц с шестью +// знаками после запятой (вставляет точку после третьего разряда) — численно это +// ровно те же герцы, что и у нас. Знак не передаётся, направление — в ZZOS. +var v: Int64; +begin + if Length(s) = 9 then begin + // Нечисловое поле — ошибка формата: StrToIntDef молча обнулил бы сдвиг. + if not TryStrToInt64(s, v) then Exit(CAT_ERROR); + if Assigned(FCtx.SetFMRptOffset) then FCtx.SetFMRptOffset(v); + Result := ''; + end else if Length(s) = 0 then + Result := Pad(Round(SafeGetFMRptOffset), 9) + else + Result := CAT_ERROR; +end; + function TCATEngine.CmdPC(const s: string): string; // PC: TX power 0-100 % (3 digits) → DriveLevel var v: Integer; @@ -969,10 +1205,11 @@ begin end; function TCATEngine.CmdPR(const s: string): string; -// PR: speech compressor — STUB: always off +// PR: речевой компрессор вкл/выкл. У Thetis команда только читает (всегда '0'); +// у нас компрессор настоящий, поэтому SET тоже работает — расширение вверх, +// кенвудовские клиенты от него не ломаются. begin - if Length(s) = 0 then Result := '0' - else Result := CAT_ERROR; + Result := OnOff(s, FCtx.GetCompOn, FCtx.SetCompOn); end; function TCATEngine.CmdPS(const s: string): string; @@ -998,10 +1235,10 @@ begin end; function TCATEngine.CmdRD(const s: string): string; -// RD: decrease RIT — maps to tune down +// RD: убавить RIT (аргумент — 5 цифр смещения) — STUB: расстройки приёма нет. +// Шаг настройки вниз — это DN, перестройка — FA/FB. begin - if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown; - Result := ''; + if (Length(s) = 0) or (Length(s) = 5) then Result := '' else Result := CAT_ERROR; end; function TCATEngine.CmdRT(const s: string): string; @@ -1013,10 +1250,9 @@ begin end; function TCATEngine.CmdRU(const s: string): string; -// RU: increase RIT — maps to tune up +// RU: прибавить RIT — STUB, см. RD. begin - if Assigned(FCtx.DoTuneUp) then FCtx.DoTuneUp; - Result := ''; + if (Length(s) = 0) or (Length(s) = 5) then Result := '' else Result := CAT_ERROR; end; function TCATEngine.CmdRX(const s: string): string; @@ -1027,7 +1263,9 @@ begin end; function TCATEngine.CmdSH(const s: string): string; -// SH: high pass filter setting (2 digits, 0-based index) — maps to filter index +// SH: верхняя кромка фильтра, индекс пресета (2 цифры). ⚠ У нас SH и SL ходят +// в ОДИН и тот же индекс пресета: раздельных «сторон» у главного приёмника нет. +// Кромки в герцах — это ZZFL/ZZFH. var idx: Integer; begin if Length(s) = 2 then begin @@ -1041,22 +1279,22 @@ begin end; function TCATEngine.CmdSL(const s: string): string; -// SL: low pass filter setting — mirrors SH +// SL: нижняя кромка фильтра — тот же индекс пресета, что и 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 +// SM: S-метр. Запрос — один символ-селектор приёмника, ответ — ПЯТЬ цифр +// (шкала Kenwood 00000..00030: до 00015 это S0..S9, дальше +1 за каждый дБ). +// Селектор Thetis шлёт как '0' или '2', TS-2000 — как '0'/'1'; принимаем все: +// второго приёмника нет, отвечаем одним и тем же. var sm: Integer; begin - if (Length(s) = 1) and ((s = '0') or (s = '1')) then begin + if (Length(s) = 1) and (s[1] in ['0', '1', '2']) then begin sm := SMeterRaw(SafeGetSMeter); - // Scale 0-260 → 0-30 (Kenwood SM scale 0000-0030) - sm := Round(sm * 30 / 260); - Result := Pad(sm, 4); + sm := Round(sm * 30 / 260); // 0..260 (шкала ZZSM) → 0..30 + Result := Pad(sm, 5); end else Result := CAT_ERROR; end; @@ -1111,9 +1349,9 @@ begin 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 = 'AD' then ans := '' // STUB: сдвиг VFO A вниз на nn шагов — см. ZZAU (вверх) + else if ext = 'AE' then ans := '' // STUB: VFO A вниз на nn пресетных шагов — нет глобального шага, см. ZZAC + else if ext = 'AF' then ans := '' // STUB: VFO A вверх на nn пресетных шагов — см. ZZAE else if ext = 'AG' then ans := ZZAG(s) else if ext = 'AI' then ans := ZZAI(s) else if ext = 'AP' then ans := ZZAP(s) @@ -1121,220 +1359,224 @@ begin else if ext = 'AS' then ans := ZZAS(s) else if ext = 'AT' then ans := ZZAT(s) else if ext = 'AU' then ans := ZZAU(s) - else if ext = 'AY' then ans := '' // STUB: APF type + else if ext = 'AY' then ans := '' // STUB: тип APF — нет аудиопикового фильтра else if ext = 'BA' then ans := ZZBA - else if ext = 'BB' then ans := '' // STUB: band button state + else if ext = 'BB' then ans := '' // STUB: bandswitch RX2 вверх — RX2 нет else if ext = 'BD' then begin ZZBD; ans := ''; end else if ext = 'BE' then ans := ZZBE(s) - else if ext = 'BF' then ans := '' // STUB: sub-RX filter - else if ext = 'BG' then ans := '' // STUB: sub-RX gain + else if ext = 'BF' then ans := '' // STUB: VFO B вверх на nn пресетных шагов — см. ZZAE + else if ext = 'BG' then ans := '' // STUB: группа диапазонов HF/VHF — не разделяем else if ext = 'BI' then ans := ZZBI(s) else if ext = 'BM' then ans := ZZBM(s) else if ext = 'BP' then ans := ZZBP(s) - else if ext = 'BR' then ans := '' // STUB: sub-RX enable + else if ext = 'BR' then ans := '' // STUB: кнопка BCI Rejection — такого фильтра нет else if ext = 'BS' then ans := ZZBS(s) - else if ext = 'BT' then ans := '' // STUB: band TX + else if ext = 'BT' then ans := '' // STUB: диапазон RX2 — RX2 нет else if ext = 'BU' then begin ZZBU; ans := ''; end - else if ext = 'BY' then ans := '' // STUB: VFO bypass + else if ext = 'BY' then ans := '' // STUB: выключение консоли — по CAT не гасимся else if ext = 'CB' then ans := ZZCB(s) else if ext = 'CD' then ans := ZZCD(s) - else if ext = 'CF' then ans := '' // STUB: 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 = 'CF' then ans := '' // STUB: показывать частоту телеграфа (UI Thetis) + else if ext = 'CI' then ans := ZZCI(s) + else if ext = 'CL' then ans := ZZCL(s) + else if ext = 'CM' then ans := ZZCM(s) else if ext = 'CN' then ans := ZZCN(s) - else if ext = 'CO' then ans := '' // STUB: compressor out - else if ext = 'CP' then ans := '' // STUB: compressor peak + else if ext = 'CO' then ans := '' // STUB: CTUN для RX2 — RX2 нет (CTUN RX1 — это ZZCN) + else if ext = 'CP' then ans := '' // STUB: компандер (у нас его нет — есть COMP, см. ZZPK) else if ext = 'CS' then ans := ZZCS(s) - else if ext = 'CT' then ans := '' // STUB: compressor threshold + else if ext = 'CT' then ans := '' // STUB: порог компандера else if ext = 'CU' then begin ZZDU; ans := ''; end else if ext = 'DA' then ans := ZZDA(s) else if ext = 'DB' then ans := ZZDB(s) - else if ext = 'DC' then ans := '' // STUB: 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 = 'DC' then ans := '' // STUB: усиление RX2 в окне Diversity — диверсити нет + else if ext = 'DD' then ans := '' // STUB: фаза в окне Diversity — диверсити нет + else if ext = 'DE' then ans := '' // STUB: кнопка Enable в окне Diversity — диверсити нет + else if ext = 'DF' then ans := '' // STUB: открыть/закрыть окно Diversity — диверсити нет + else if ext = 'DG' then ans := '' // STUB: усиление RX в окне Diversity — диверсити нет + else if ext = 'DH' then ans := '' // STUB: источник RX в окне Diversity — диверсити нет else if ext = 'DM' then ans := ZZDM(s) else if ext = 'DN' then ans := ZZDN(s) - else if ext = 'DO' then ans := '' // STUB: 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 = 'DO' then ans := '' // STUB: верхний уровень водопада — не проброшено (см. ZZDN) + else if ext = 'DP' then ans := '' // STUB: верх сетки спектра — не проброшено + else if ext = 'DQ' then ans := '' // STUB: низ сетки спектра — не проброшено + else if ext = 'DR' then ans := '' // STUB: шаг сетки спектра — не проброшено else if ext = 'DU' then begin ZZDU; ans := ''; end - else if ext = 'DX' then ans := '' // STUB: sub NB + else if ext = 'DX' then ans := '' // STUB: кнопка DX (споты кластера) — флаг живёт в UI, не в контроллере else if ext = 'EA' then ans := ZZEA(s) else if ext = 'EB' then ans := ZZEB(s) else if ext = 'EM' then ans := ZZEM(s) - else if ext = 'ER' then ans := '' // STUB: EQ reset - else if ext = 'ET' then ans := '' // STUB: EQ TX + else if ext = 'ER' then ans := '' // STUB: кнопка RXEQ — эквалайзер приёма + else if ext = 'ET' then ans := ZZET(s) else if ext = 'FA' then ans := ZZFA(s) else if ext = 'FB' then ans := ZZFB(s) else if ext = 'FD' then ans := ZZFD(s) else if ext = 'FH' then ans := ZZFH(s) else if ext = 'FI' then ans := ZZFI(s) - else if ext = 'FJ' then ans := '' // STUB: RX2 filter + else if ext = 'FJ' then ans := '' // STUB: фильтр RX2 — RX2 нет else if ext = 'FL' then ans := ZZFL(s) else if ext = 'FM' then ans := ZZFM - else if ext = 'FR' then ans := '' // STUB: filter RX - else if ext = 'FS' then ans := '' // STUB: sub filter + else if ext = 'FR' then ans := '' // STUB: верхняя кромка фильтра RX2 — RX2 нет + else if ext = 'FS' then ans := '' // STUB: нижняя кромка фильтра RX2 — RX2 нет else if ext = 'FT' then ans := ZZFT(s) - else if ext = 'FV' then ans := '' // STUB: 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 = 'FV' then ans := '' // STUB: чтение FlexWire (1 байт) — шины нет + else if ext = 'FW' then ans := '' // STUB: чтение FlexWire (2 байта) — шины нет + else if ext = 'FX' then ans := '' // STUB: запись FlexWire (1 байт) — шины нет + else if ext = 'FY' then ans := '' // STUB: запись FlexWire (2 байта) — шины нет else if ext = 'GA' then ans := ZZGA(s) - else if ext = 'GE' then ans := '' // STUB: gain enable - else if ext = 'GL' then ans := '' // STUB: gain level + else if ext = 'GE' then ans := '' // STUB: кнопка noise gate — такого блока нет + else if ext = 'GL' then ans := '' // STUB: уровень noise gate — такого блока нет else if ext = 'GR' then ans := ZZGR(s) else if ext = 'GT' then ans := ZZGT(s) - else if ext = 'GU' then ans := '' // STUB: 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 = 'GU' then ans := '' // STUB: постоянная AGC для RX2 — RX2 нет + else if ext = 'HA' then ans := '' // STUB: размер аудиобуфера — по CAT не крутим + else if ext = 'HR' then ans := '' // STUB: размер буфера DSP RX (телефония) — по CAT не крутим + else if ext = 'HT' then ans := '' // STUB: размер буфера DSP TX (телефония) — по CAT не крутим + else if ext = 'HU' then ans := '' // STUB: размер буфера DSP RX (CW) — по CAT не крутим + else if ext = 'HV' then ans := '' // STUB: размер буфера DSP TX (CW) — по CAT не крутим + else if ext = 'HW' then ans := '' // STUB: размер буфера DSP RX (digital) — по CAT не крутим + else if ext = 'HX' then ans := '' // STUB: размер буфера DSP TX (digital) — по CAT не крутим else if ext = 'ID' then ans := ZZID else if ext = 'IF' then ans := ZZIF(s) else if ext = 'IO' then ans := ZZIO else if ext = 'IS' then ans := ZZIS(s) - else if ext = 'IT' then ans := '' // STUB: 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 = 'IT' then ans := '' // STUB: сдвиг IF — полосу двигаем кромками (ZZFL/ZZFH) + else if ext = 'IU' then ans := '' // STUB: сброс сдвига фильтра — см. ZZIT + else if ext = 'JO' then ans := '' // STUB: нет такой команды у Thetis + else if ext = 'JP' then ans := '' // STUB: служебная JSON-команда Thetis + else if ext = 'JQ' then ans := '' // STUB: служебная JSON-команда Thetis + else if ext = 'JR' then ans := '' // STUB: служебная JSON-команда Thetis + else if ext = 'JS' then ans := '' // STUB: служебная JSON-команда Thetis else if ext = 'KM' then ans := ZZKM(s) - else if ext = 'KO' then ans := '' // STUB: key on + else if ext = 'KO' then ans := '' // STUB: без описания у Thetis else if ext = 'KS' then ans := ZZKS(s) else if ext = 'KY' then ans := ZZKY(s) - else if ext = 'LA' then ans := '' // STUB: 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 = 'LA' then ans := '' // STUB: уровень RX0 — микшер Thetis + else if ext = 'LB' then ans := '' // STUB: стереобаланс главного приёмника — микшера нет + else if ext = 'LC' then ans := '' // STUB: уровень sub-RX — sub-RX нет + else if ext = 'LD' then ans := '' // STUB: стереобаланс sub-RX — sub-RX нет + else if ext = 'LE' then ans := '' // STUB: уровень RX2 — RX2 нет + else if ext = 'LF' then ans := '' // STUB: стереобаланс RX2 — RX2 нет + else if ext = 'LG' then ans := '' // STUB: автомьют RX1 при TX на VFO B — такой автоматики нет + else if ext = 'LH' then ans := '' // STUB: автомьют RX2 при TX на VFO A — RX2 нет + else if ext = 'LI' then ans := ZZLI(s) else if ext = 'MA' then ans := ZZMA(s) else if ext = 'MB' then ans := ZZMB(s) else if ext = 'MD' then ans := ZZMD(s) - else if ext = 'ME' then ans := '' // STUB: mode enable - else if ext = 'MF' then ans := '' // STUB: mode freq + else if ext = 'ME' then ans := '' // STUB: без описания у Thetis + else if ext = 'MF' then ans := '' // STUB: текст на LCD мультифункционального энкодера Andromeda — не реализовано else if ext = 'MG' then ans := ZZMG(s) else if ext = 'ML' then ans := ZZML else if ext = 'MN' then ans := ZZMN(s) else if ext = 'MO' then ans := ZZMO(s) - else if ext = 'MR' then ans := '' // STUB: 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 = 'MR' then ans := '' // STUB: режим измерителя приёма — метр один + else if ext = 'MS' then ans := '' // STUB: галка MultiRX Swap — MultiRX нет + else if ext = 'MT' then ans := '' // STUB: режим измерителя передачи — метр один + else if ext = 'MU' then ans := '' // STUB: кнопка MultiRX — MultiRX нет else if ext = 'MV' then ans := ZZMV - else if ext = 'MW' then ans := '' // STUB: mode write - else if ext = 'MX' then ans := '' // STUB: mode extended + else if ext = 'MW' then ans := '' // STUB: удалить канал памяти — память в CAT не проброшена + else if ext = 'MX' then ans := '' // STUB: восстановить канал памяти n — см. ZZMW else if ext = 'MY' then ans := ZZMY - else if ext = 'MZ' then ans := '' // STUB: mode Z + else if ext = 'MZ' then ans := '' // STUB: записать конфигурацию в канал памяти n — см. ZZMW else if ext = 'NA' then ans := ZZNA(s) else if ext = 'NB' then ans := ZZNB(s) - else if ext = 'NC' then ans := '' // STUB: 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 = 'NC' then ans := '' // STUB: NB для RX2 — RX2 нет + else if ext = 'ND' then ans := '' // STUB: NB2 для RX2 — RX2 нет + else if ext = 'NE' then ans := '' // STUB: статус NR RX1 (0..4) — дублирует ZZNR, не проброшено + else if ext = 'NF' then ans := '' // STUB: статус NR для RX2 — RX2 нет + else if ext = 'NG' then ans := '' // STUB: глубина подавления NR4 — такого алгоритма нет + else if ext = 'NH' then ans := '' // STUB: глубина подавления NR4 (вторая) — см. ZZNG else if ext = 'NL' then ans := ZZNL(s) - else if ext = 'NM' then ans := '' // STUB: NR mode + else if ext = 'NM' then ans := '' // STUB: порог NB2 — порог бланкера не вынесен в настройки else if ext = 'NN' then ans := ZZNN(s) - else if ext = 'NO' then ans := '' // STUB: sub NR + else if ext = 'NO' then ans := '' // STUB: спектральный бланкер RX2 — RX2 нет (RX1 — это ZZNN) else if ext = 'NR' then ans := ZZNR(s) else if ext = 'NS' then ans := ZZNS(s) else if ext = 'NT' then ans := ZZNT(s) - else if ext = 'NU' then ans := '' // STUB: NR update - else if ext = 'NV' then ans := '' // STUB: NR value - else if ext = 'NW' then ans := '' // STUB: NR write + else if ext = 'NU' then ans := '' // STUB: ANF для RX2 — RX2 нет + else if ext = 'NV' then ans := '' // STUB: статус шумоподавителя — дублирует ZZNR + else if ext = 'NW' then ans := '' // STUB: кнопка NR2 для RX2 — RX2 нет else if ext = 'OA' then ans := ZZOA(s) else if ext = 'OB' then ans := ZZOB(s) - else if ext = 'OC' then ans := '' // STUB: 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 = 'OC' then ans := '' // STUB: антенна передачи — порты AD936x в CAT не проброшены + else if ext = 'OD' then ans := '' // STUB: режим антенн — см. ZZOC + else if ext = 'OE' then ans := '' // STUB: галка внешней антенны RX1 — см. ZZOC + else if ext = 'OF' then ans := '' // STUB: реле TX на разъёме RCA — управления реле нет + else if ext = 'OG' then ans := '' // STUB: разрешение задержек реле TX — см. ZZOF + else if ext = 'OH' then ans := '' // STUB: величины задержек реле TX — см. ZZOF + else if ext = 'OJ' then ans := '' // STUB: без описания у Thetis + else if ext = 'OL' then ans := '' // STUB: click-tune офсет для DIGL — офсетов режима нет + else if ext = 'OS' then ans := ZZOS(s) + else if ext = 'OT' then ans := ZZOT(s) + else if ext = 'OU' then ans := '' // STUB: click-tune офсет для DIGU — см. ZZOL + else if ext = 'OV' then ans := '' // STUB: кнопка ATU — тюнера нет + else if ext = 'OW' then ans := '' // STUB: кнопка ATU Bypass — тюнера нет + else if ext = 'OX' then ans := '' // STUB: состояние настройки ATU Aries — тюнера нет + else if ext = 'OZ' then ans := '' // STUB: сброс состояния ATU Aries — тюнера нет + else if ext = 'PA' then ans := '' // STUB: преамп/аттенюатор — не проброшено (аттенюатор RX1 — ZZRX) + else if ext = 'PB' then ans := '' // STUB: преамп RX2 — RX2 нет else if ext = 'PC' then ans := ZZPC(s) else if ext = 'PD' then ans := ZZPD - else if ext = 'PE' then ans := '' // STUB: power enable + else if ext = 'PE' then ans := '' // STUB: пан дисплея — не проброшено + else if ext = 'PK' then ans := ZZPK(s) + else if ext = 'PL' then ans := ZZPL(s) else if ext = 'PO' then ans := ZZPO(s) else if ext = 'PS' then ans := ZZPS(s) - else if ext = 'PY' then ans := '' // STUB: power Y - else if ext = 'PZ' then ans := '' // STUB: power Z - else if ext = 'QK' then ans := '' // STUB: query keep + else if ext = 'PY' then ans := '' // STUB: зум дисплея — не проброшено + else if ext = 'PZ' then ans := '' // STUB: кнопки зума дисплея — не проброшено + // ZZQK: у Thetis break-in делится на semi и QSK; у нас break-in один + // (прошивочный), а разница semi/QSK выражается hang-time'ом — ZZCD. + else if ext = 'QK' then ans := '' // STUB: нет отдельного переключателя semi/QSK else if ext = 'QM' then ans := ZZQM else if ext = 'QR' then ans := ZZQR else if ext = 'QS' then ans := ZZQS else if ext = 'RA' then ans := ZZRA(s) - else if ext = 'RB' then ans := '' // STUB: RIT B + else if ext = 'RB' then ans := '' // STUB: галка RTTY-офсета для VFO B — RTTY-движка нет else if ext = 'RC' then begin ZZRC; ans := ''; end else if ext = 'RD' then begin ZZRD(s); ans := ''; end - else if ext = 'RF' then ans := '' // STUB: RIT freq - else if ext = 'RH' then ans := '' // STUB: RIT high + else if ext = 'RF' then ans := '' // STUB: величина RIT — расстройки приёма нет + else if ext = 'RH' then ans := '' // STUB: частота офсета RTTY DIGH — RTTY-движка нет else if ext = 'RL' then ans := ZZRL(s) - else if ext = 'RM' then ans := '' // STUB: RIT mode - else if ext = 'RS' then ans := '' // STUB: RIT status - else if ext = 'RT' then ans := '' // STUB: RIT transmit + else if ext = 'RM' then ans := '' // STUB: показание измерителя приёма — читается через ZZSM + else if ext = 'RS' then ans := '' // STUB: кнопка RX2 — RX2 нет + else if ext = 'RT' then ans := '' // STUB: кнопка RIT — расстройки приёма нет else if ext = 'RU' then begin ZZRU(s); ans := ''; end else if ext = 'RV' then ans := ZZRV else if ext = 'RX' then ans := ZZRX(s) - else if ext = 'RY' then ans := '' // STUB: RIT Y + else if ext = 'RY' then ans := '' // STUB: шаговый аттенюатор RX2 — RX2 нет (RX1 — это ZZRX) else if ext = 'SA' then begin ZZSA; ans := ''; end else if ext = 'SB' then begin ZZSB; ans := ''; end - else if ext = 'SD' then ans := '' // STUB: 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 = 'SD' then ans := '' // STUB: шаг настройки вниз колесом — это ZZSA + else if ext = 'SF' then ans := '' // STUB: фильтр «центр+ширина» одной командой — не проброшено + else if ext = 'SG' then ans := '' // STUB: VFO B вниз на один шаг — не проброшено (по индексу — ZZBM) + else if ext = 'SH' then ans := '' // STUB: VFO B вверх на один шаг — не проброшено (по индексу — ZZBP) else if ext = 'SM' then ans := ZZSM(s) - else if ext = 'SN' then ans := '' // STUB: serial number + else if ext = 'SN' then ans := '' // STUB: серийный номер радио — не проброшено else if ext = 'SO' then ans := ZZSO(s) else if ext = 'SP' then ans := ZZSP(s) else if ext = 'SQ' then ans := ZZSQ(s) else if ext = 'SR' then ans := ZZSR(s) - else if ext = 'SS' then ans := '' // STUB: sub-RX + else if ext = 'SS' then ans := '' // STUB: без описания у Thetis else if ext = 'ST' then ans := ZZST(s) else if ext = 'SU' then begin ZZSU; ans := ''; end - else if ext = 'SV' then ans := '' // STUB: 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 = 'SV' then ans := '' // STUB: шумоподавитель RX2 — RX2 нет + else if ext = 'SW' then ans := '' // STUB: обмен кнопок TX VFO A/B — не проброшено (обмен VFO — ZZVS) + else if ext = 'SX' then ans := '' // STUB: порог шумоподавителя RX2 — RX2 нет + else if ext = 'SY' then ans := '' // STUB: кнопка VFO Sync — синхронизации VFO нет + else if ext = 'SZ' then ans := '' // STUB: подгон VFO под шаг настройки — см. ZZSY else if ext = 'TA' then ans := ZZTA(s) else if ext = 'TB' then ans := ZZTB(s) - else if ext = 'TF' then ans := '' // STUB: 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 = 'TF' then ans := '' // STUB: показывать полосу TX на спектре (UI Thetis) + else if ext = 'TH' then ans := ZZTH(s) + else if ext = 'TI' then ans := '' // STUB: запрет выхода мощности (внешние антенны/тюнер) + else if ext = 'TL' then ans := ZZTL(s) + else if ext = 'TM' then ans := ZZTM(s) + else if ext = 'TO' then ans := ZZTO(s) + else if ext = 'TP' then ans := ZZTP(s) + else if ext = 'TS' then ans := '' // STUB: датчик температуры Flex 5000 + else if ext = 'TU' then ans := ZZTU(s) + else if ext = 'TV' then ans := '' // STUB: TX-частота при split с RX2 else if ext = 'TX' then ans := ZZTX(s) - else if ext = 'UA' then ans := '' // STUB: update A + else if ext = 'UA' then ans := '' // STUB: имена диапазонов трансвертера — не проброшено else if ext = 'UP' then ans := ZZUP(s) else if ext = 'US' then ans := ZZUS else if ext = 'UT' then ans := ZZUT(s) @@ -1342,30 +1584,30 @@ begin 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 = 'VC' then ans := '' // STUB: усиление передачи в VAC — VAC нет + else if ext = 'VD' then ans := '' // STUB: частота дискретизации VAC — VAC нет + else if ext = 'VE' then ans := '' // STUB: кнопка VOX — VOX нет else if ext = 'VF' then ans := ZZFA(s) // alias ZZFA else if ext = 'VG' then ans := ZZVG(s) - else if ext = 'VH' then ans := '' // STUB: 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 = 'VH' then ans := '' // STUB: галка I/Q в VAC — VAC нет + else if ext = 'VI' then ans := '' // STUB: входной кабель VAC — VAC нет + else if ext = 'VJ' then ans := '' // STUB: галка Direct I/Q Use RX2 — RX2 нет + else if ext = 'VK' then ans := '' // STUB: галка VAC2 Enable — VAC нет + else if ext = 'VL' then ans := '' // STUB: кнопка блокировки VFO — не проброшено (SetVfoLock есть) + else if ext = 'VM' then ans := '' // STUB: драйвер VAC — VAC нет else if ext = 'VN' then ans := ZZVN - else if ext = 'VO' then ans := '' // STUB: 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 = 'VO' then ans := '' // STUB: выходной кабель VAC — VAC нет + else if ext = 'VP' then ans := '' // STUB: галка VAC1 IQ Calibrate — VAC нет + else if ext = 'VQ' then ans := '' // STUB: драйвер VAC2 — VAC нет + else if ext = 'VR' then ans := '' // STUB: входной кабель VAC2 — VAC нет else if ext = 'VS' then ans := ZZVS(s) - else if ext = 'VT' then ans := '' // STUB: 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 = 'VT' then ans := '' // STUB: выходной кабель VAC2 — VAC нет + else if ext = 'VU' then ans := '' // STUB: частота дискретизации VAC2 — VAC нет + else if ext = 'VV' then ans := '' // STUB: галка VAC2 Stereo — VAC нет + else if ext = 'VW' then ans := '' // STUB: без описания у Thetis + else if ext = 'VX' then ans := '' // STUB: усиление передачи в VAC2 — VAC нет + else if ext = 'VY' then ans := '' // STUB: размер буфера VAC1 — VAC нет + else if ext = 'VZ' then ans := '' // STUB: размер буфера VAC1 (второй) — VAC нет else if ext = 'WA' then ans := ZZWA(s) else if ext = 'WB' then ans := ZZWB(s) else if ext = 'WC' then ans := ZZWC(s) @@ -1384,21 +1626,21 @@ begin 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 = 'WT' then ans := '' // STUB: уровень микрофона в микшере F1500 — железо другого вендора + else if ext = 'WU' then ans := '' // STUB: уровень входа FireWire в микшере F1500 — см. ZZWT + else if ext = 'WV' then ans := '' // STUB: уровень наушников в микшере F1500 — см. ZZWT + else if ext = 'WW' then ans := '' // STUB: уровень выхода FlexWire в микшере F1500 — см. ZZWT else if ext = 'XA' then ans := ZZXA(s) else if ext = 'XC' then ans := ZZXC - else if ext = 'XD' then ans := '' // STUB: extended D + else if ext = 'XD' then ans := '' // STUB: убавить XIT — расстройки передачи нет 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 = 'XH' then ans := '' // STUB: задержка VOX — VOX нет + else if ext = 'XN' then ans := '' // STUB: составной статус RX1 — не собираем + else if ext = 'XO' then ans := '' // STUB: составной статус RX2 — RX2 нет + else if ext = 'XS' then ans := '' // STUB: кнопка XIT — расстройки передачи нет + else if ext = 'XT' then ans := '' // STUB: кнопка X2TR — такого режима нет + else if ext = 'XU' then ans := '' // STUB: прибавить XIT — расстройки передачи нет + else if ext = 'XV' then ans := '' // STUB: составной статус VFO — не собираем else if ext = 'YA' then ans := ZZYA(s) else if ext = 'YB' then ans := ZZYB(s) else if ext = 'YC' then ans := ZZYC(s) @@ -1408,7 +1650,7 @@ begin else if ext = 'ZD' then ans := ZZZD(s) else if ext = 'ZE' then ans := ZZZE(s) else if ext = 'ZM' then ans := ZZZM(s) - else if ext = 'ZN' then ans := '' // STUB: system N + else if ext = 'ZN' then ans := '' // STUB: быстрый split вкл/выкл — не проброшено else if ext = 'ZO' then ans := ZZZO(s) else if ext = 'ZP' then ans := ZZZP(s) else if ext = 'ZQ' then ans := ZZZQ(s) @@ -1432,28 +1674,27 @@ end; ══════════════════════════════════════════════════════════════════════════ } function TCATEngine.ZZAA(const s: string): string; -// ZZAA: RX1 AF gain — alias ZZAG -begin Result := ZZAG(s); end; +// ZZAA: усиление APF — STUB: аудиопикового фильтра в EWSDR нет. +// (Громкость — это ZZAG; раньше ZZAA был ошибочно заалиашен на неё.) +begin + if Length(s) = 4 then Result := '' else if Length(s) = 0 then Result := '0000' + else Result := CAT_ERROR; +end; function TCATEngine.ZZAB(const s: string): string; -// ZZAB: RX2 AF gain — STUB: no RX2 +// ZZAB: полоса APF — STUB: аудиопикового фильтра нет. begin if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050' else Result := CAT_ERROR; end; function TCATEngine.ZZAC(const s: string): string; -// ZZAC: band selection (2-digit band index) -var idx: Integer; +// ZZAC: размер шага настройки консоли (индекс) — STUB: у EWSDR глобального +// шага нет, колесо/UP/DN ходят фиксированным шагом. +// (Выбор диапазона — это ZZBS; раньше он был заалиашен сюда.) 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; + if Length(s) = 2 then Result := '' else if Length(s) = 0 then Result := '00' + else Result := CAT_ERROR; end; function TCATEngine.ZZAG(const s: string): string; @@ -1479,8 +1720,11 @@ begin end; function TCATEngine.ZZAP(const s: string): string; -// ZZAP: TX power 0–100 -begin Result := ZZPC(s); end; +// ZZAP: кнопка APF — STUB: аудиопикового фильтра нет. +// (Мощность — это ZZPC; раньше ZZAP был ошибочно заалиашен на неё.) +begin + Result := OnOff(s, nil, nil); +end; function TCATEngine.ZZAR(const s: string): string; // ZZAR: RX1 AGC-T порог (RF gain). Формат: знак + 3 цифры (напр. "+020", "+120"). @@ -1522,7 +1766,7 @@ begin end; function TCATEngine.ZZAS(const s: string): string; -// ZZAS: sub-RX enable — STUB: no RX2 +// ZZAS: AGC-T второго приёмника — STUB: RX2 нет. begin if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' @@ -1530,7 +1774,7 @@ begin end; function TCATEngine.ZZAT(const s: string): string; -// ZZAT: antenna tuner — STUB: no ATU +// ZZAT: настройка APF — STUB: аудиопикового фильтра нет. begin if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' @@ -1538,7 +1782,7 @@ begin end; function TCATEngine.ZZBA: string; -// ZZBA: sub-RX AF gain — STUB: no RX2 +// ZZBA: bandswitch второго приёмника вниз — STUB: RX2 нет. begin Result := '050'; end; function TCATEngine.ZZBD: string; @@ -1549,7 +1793,8 @@ begin end; function TCATEngine.ZZBE(const s: string): string; -// ZZBE: band enable — STUB: all bands always enabled +// ZZBE: VFO B вниз на nn пресетных шагов — STUB: глобального размера шага у +// EWSDR нет (см. ZZAC). Шаг по индексу — это ZZBM/ZZBP. begin if Length(s) = 3 then Result := '' else if Length(s) = 2 then Result := '1' @@ -1557,26 +1802,40 @@ begin end; function TCATEngine.ZZBI(const s: string): string; -// ZZBI: filter index (2-digit, 0-9) -var idx: Integer; +// ZZBI: кнопка BIN (бинауральный приём) — STUB: такого режима нет. +// (Индекс фильтра — это ZZFI; раньше он жил здесь.) 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; + Result := OnOff(s, nil, nil); end; function TCATEngine.ZZBM(const s: string): string; -// ZZBM: band mode — set mode for current band -begin Result := ZZMD(s); end; +// ZZBM: сдвиг VFO B ВНИЗ на один шаг с индексом nn (00..14) — пара к ZZBP. +// (Режим — это ZZMD; раньше ZZBM был ошибочно заалиашен на него.) +var idx: Integer; +begin + if Length(s) = 2 then begin + idx := EnsureRange(StrToIntDef(s, 0), 0, 14); + if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(SafeGetVfoB - StepIdxToHz(idx)); + Result := ''; + end else + Result := CAT_ERROR; +end; function TCATEngine.ZZBS(const s: string): string; -// ZZBS: band select — same as ZZAC -begin Result := ZZAC(s); end; +// ZZBS: выбор диапазона. ⚠ Отклонение от Thetis: там поле трёхсимвольное и +// несёт КОД диапазона ("160"/"040"/"WWV"), у нас — двузначный ИНДЕКС в списке +// диапазонов EWSDR. Менять поздно: на этом формате уже сидят клиенты. +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.ZZBU: string; begin @@ -1585,17 +1844,18 @@ begin end; function TCATEngine.ZZCB(const s: string): string; -// ZZCB: compression bypass — STUB: TX DSP not implemented +// ZZCB: break-in — прошивка сама поднимает PTT на нажатие ключа. begin - if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' - else Result := CAT_ERROR; + Result := OnOff(s, FCtx.GetCWBreakIn, FCtx.SetCWBreakIn); end; function TCATEngine.ZZCD(const s: string): string; -// ZZCD: compressor drive — STUB: TX DSP not implemented +// ZZCD: задержка отпускания break-in (hang time), мс. Эталон принимает +// 150..5000, у нас потолок 2000 — это НАШ предел, а не описка: столько же +// стоит в настройках телеграфа и уходит в поле HangDelay пакета DUC Specific. +// Значение выше подрезается (как и всё прочее у Thetis), формат — те же 4 цифры. begin - if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050' - else Result := CAT_ERROR; + Result := NumField(s, 4, 0, CW_HANG_MAX, FCtx.GetCWHangTime, FCtx.SetCWHangTime); end; function TCATEngine.ZZCN(const s: string): string; @@ -1610,38 +1870,44 @@ begin end; function TCATEngine.ZZCS(const s: string): string; -// ZZCS: compressor status — STUB +// ZZCS: скорость кейера, WPM (два разряда). Та же величина, что у KS/ZZKS — +// у Kenwood-команды поле шире, вот и всё различие. begin - if Length(s) = 0 then Result := '0' else Result := CAT_ERROR; + Result := NumField(s, 2, CW_WPM_MIN, CW_WPM_MAX, FCtx.GetCWSpeed, FCtx.SetCWSpeed); end; function TCATEngine.ZZDA(const s: string): string; -// ZZDA: diversity A — STUB: diversity not implemented +// ZZDA: усреднение спектра вкл/выкл — STUB: в CAT не проброшено. begin if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' else Result := CAT_ERROR; end; function TCATEngine.ZZDB(const s: string): string; -// ZZDB: diversity B — STUB +// ZZDB: опорный приёмник в окне Diversity — STUB: диверсити-приёма нет. begin if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' else Result := CAT_ERROR; end; function TCATEngine.ZZDM(const s: string): string; -// ZZDM: display mode — STUB: no separate display mode setting +// ZZDM: режим дисплея (панадаптер/водопад/…) — STUB: в CAT не проброшено. begin if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' else Result := CAT_ERROR; end; function TCATEngine.ZZDN(const s: string): string; -// ZZDN: drive level — alias ZZPC -begin Result := ZZPC(s); end; +// ZZDN: нижний уровень водопада, dBm — STUB: уровни водопада живут в per-pan +// конфиге, в контроллер пока не проброшены (см. §3 doc/CAT_STATUS.md). +// (Мощность — это ZZPC; раньше ZZDN был ошибочно заалиашен на неё.) +begin + if Length(s) = 4 then Result := '' else if Length(s) = 0 then Result := '-120' + else Result := CAT_ERROR; +end; function TCATEngine.ZZDU: string; -// ZZDU: display units update — STUB +// ZZDU: составное слово состояния для DDUtil — STUB: не собираем. begin Result := ''; end; function TCATEngine.ZZEM(const s: string): string; @@ -1649,15 +1915,47 @@ function TCATEngine.ZZEM(const s: string): string; begin Result := CAT_ERROR; end; function TCATEngine.ZZEA(const s: string): string; -// ZZEA: RX EQ profile name — STUB: no EQ +// ZZEA: значения эквалайзера ПРИЁМА — STUB: RX-эквалайзера нет (TX — ZZEB). begin if Length(s) > 0 then Result := '' else Result := ''; // no EQ end; function TCATEngine.ZZEB(const s: string): string; -// ZZEB: TX EQ profile name — STUB: TX DSP not implemented +// ZZEB: значения TX-эквалайзера одной строкой (формат Thetis). +// nnn + (nnn){nb+1}, добито нулями до 36 символов; +// nb — число полос, следом преамп и полосы, отрицательные как '-nn'. +var + G: TCATEQGains; + nb, i, v: Integer; + ans, fld: string; begin - if Length(s) > 0 then Result := '' else Result := ''; + if Length(s) = 0 then + begin + FillChar(G, SizeOf(G), 0); + if Assigned(FCtx.GetTXEQ) then nb := FCtx.GetTXEQ(G) else nb := 0; + nb := EnsureRange(nb, 0, High(G)); + ans := Pad(nb, 3); + for i := 0 to nb do + begin + v := EnsureRange(G[i], EQ_GAIN_MIN, EQ_GAIN_MAX); + if v < 0 then ans := ans + '-' + Pad(-v, 2) + else ans := ans + Pad(v, 3); + end; + while Length(ans) < 36 do ans := ans + '0'; + Exit(Copy(ans, 1, 36)); + end; + if Length(s) <> 36 then Exit(CAT_ERROR); + if not TryStrToInt(Copy(s, 1, 3), nb) then Exit(CAT_ERROR); + if (nb < 0) or (nb > High(G)) then Exit(CAT_ERROR); + FillChar(G, SizeOf(G), 0); + for i := 0 to nb do + begin + fld := Copy(s, 4 + i * 3, 3); + if not TryStrToInt(fld, v) then Exit(CAT_ERROR); + G[i] := EnsureRange(v, EQ_GAIN_MIN, EQ_GAIN_MAX); + end; + if Assigned(FCtx.SetTXEQ) then FCtx.SetTXEQ(nb, G); + Result := ''; end; function TCATEngine.ZZFA(const s: string): string; @@ -1685,22 +1983,44 @@ begin end; function TCATEngine.ZZFD(const s: string): string; -// ZZFD: FM deviation — STUB: FM deviation not configurable +// ZZFD: девиация передачи — радиокнопка «узкая/широкая» (2.5/5 кГц). Поле в +// профиле непрерывное, поэтому на чтении делим по середине: всё, что ниже +// 5 кГц, отвечает узкой. begin - if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '1' - else Result := CAT_ERROR; + if Length(s) = 0 then + Exit(B2C(SafeGetFMDeviation >= FM_DEV_WIDE)); + if (s <> '0') and (s <> '1') then Exit(CAT_ERROR); + if Assigned(FCtx.SetFMDeviation) then + begin + if s = '1' then FCtx.SetFMDeviation(FM_DEV_WIDE) + else FCtx.SetFMDeviation(FM_DEV_NARROW); + end; + Result := ''; end; function TCATEngine.ZZFI(const s: string): string; -// ZZFI: filter index (2-digit) -begin Result := ZZBI(s); end; +// ZZFI: индекс текущего фильтра (2 цифры). +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.ZZFL(const s: string): string; -// ZZFL: filter low cut (signed Hz, e.g. "-0150"). Работает, если контекст умеет -// кромки (слайс-CAT); у главного приёмника фильтр индексный — заглушка. +// ZZFL: нижняя кромка фильтра, Гц со знаком — поле фиксированное, ровно пять +// символов («-0150»). Нечисловой ввод раньше проваливался через StrToIntDef в +// ноль и МОЛЧА схлопывал кромку; теперь это ошибка формата. +var v: Integer; begin - if Length(s) >= 4 then begin - if Assigned(FCtx.SetFilterLow) then FCtx.SetFilterLow(StrToIntDef(s, 0)); + if Length(s) = 5 then begin + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + if Assigned(FCtx.SetFilterLow) then FCtx.SetFilterLow(v); Result := ''; end else if Length(s) = 0 then begin if Assigned(FCtx.GetFilterLow) then Result := SignedPad(FCtx.GetFilterLow, 4) @@ -1711,10 +2031,12 @@ begin end; function TCATEngine.ZZFH(const s: string): string; -// ZZFH: filter high cut (signed Hz) — см. ZZFL. +// ZZFH: верхняя кромка фильтра, Гц со знаком — формат и разбор как у ZZFL. +var v: Integer; begin - if Length(s) >= 4 then begin - if Assigned(FCtx.SetFilterHigh) then FCtx.SetFilterHigh(StrToIntDef(s, 0)); + if Length(s) = 5 then begin + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + if Assigned(FCtx.SetFilterHigh) then FCtx.SetFilterHigh(v); Result := ''; end else if Length(s) = 0 then begin if Assigned(FCtx.GetFilterHigh) then Result := SignedPad(FCtx.GetFilterHigh, 4) @@ -1731,11 +2053,14 @@ begin end; function TCATEngine.ZZFT(const s: string): string; -// ZZFT: TX frequency — same as ZZFA (no split yet) -begin Result := ZZFA(s); end; +// ZZFT: частота передачи. Со split'ом передача идёт с VFO B — туда и адресуем, +// иначе на VFO A. (Раньше команда всегда била в VFO A: split ещё не было.) +begin + if SafeGetSplit then Result := ZZFB(s) else Result := ZZFA(s); +end; function TCATEngine.ZZGA(const s: string): string; -// ZZGA: GUID add (TCP client ID management) — handled by TCP server layer +// ZZGA: регистрация GUID клиента — адресацией ответов ведает слой TCP. // If called from serial, just acknowledge begin if Length(s) = 36 then Result := s @@ -1743,7 +2068,7 @@ begin end; function TCATEngine.ZZGR(const s: string): string; -// ZZGR: GUID remove (TCP client ID) — handled by TCP server layer +// ZZGR: снятие GUID клиента — см. ZZGA. begin if Length(s) = 36 then Result := s else Result := CAT_ERROR; @@ -1767,40 +2092,35 @@ 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) +// ZZIF: составной статус трансивера — то же, что кенвудовская IF. begin if Length(s) = 0 then Result := CmdIF else Result := CAT_ERROR; end; function TCATEngine.ZZIO: string; -// ZZIO: I/O status — STUB +// ZZIO: список установленных опций — STUB: опций не заводим. begin Result := '0'; end; function TCATEngine.ZZIS(const s: string): string; -// ZZIS: initial setup flag — STUB +// ZZIS: ширина IF — STUB: полоса задаётся кромками (ZZFL/ZZFH) и пресетами. begin if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '1' else Result := CAT_ERROR; end; function TCATEngine.ZZKM(const s: string): string; -// ZZKM: режим кейера (0=прямой ключ, 1=iambic A, 2=iambic B). +// ZZKM: запустить CW-макрос номер n (1..9) — у нас это ячейка памяти сообщений +// (окно CW Messages, F1..F8). Write-only, как в Thetis. +// Раньше здесь сидел режим кейера — своей команды у него в протоколе нет, +// иамбик вкл/выкл доступен через ZZCI. var v: Integer; begin - if Length(s) = 1 then - begin - if not TryStrToInt(s, v) then Exit(CAT_ERROR); - if (v < 0) or (v > 2) then Exit(CAT_ERROR); - if Assigned(FCtx.SetCWKeyerMode) then FCtx.SetCWKeyerMode(v); - Result := ''; - end - else if Length(s) = 0 then - begin - if Assigned(FCtx.GetCWKeyerMode) then Result := IntToStr(FCtx.GetCWKeyerMode) - else Result := '0'; - end - else Result := CAT_ERROR; + if Length(s) <> 1 then Exit(CAT_ERROR); + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + if (v < 1) or (v > 9) then Exit(CAT_ERROR); + if Assigned(FCtx.SendCWMacro) then FCtx.SendCWMacro(v); + Result := ''; end; function TCATEngine.ZZKS(const s: string): string; @@ -1816,12 +2136,17 @@ begin end; function TCATEngine.ZZMA(const s: string): string; -// ZZMA: RX1 mode -begin Result := ZZMD(s); end; +// ZZMA: кнопка MUT (заглушить приём). +// (Режим — это ZZMD; раньше ZZMA был ошибочно заалиашен на него.) +begin + Result := OnOff(s, FCtx.GetMuted, FCtx.SetMuted); +end; function TCATEngine.ZZMB(const s: string): string; -// ZZMB: RX2 mode — STUB: no RX2; returns same as RX1 -begin Result := ZZMD(s); end; +// ZZMB: кнопка MUT второго приёмника — STUB: RX2 нет. +begin + Result := OnOff(s, nil, nil); +end; function TCATEngine.ZZMD(const s: string): string; // ZZMD: mode (1 digit Kenwood code, same as MD) @@ -1838,10 +2163,21 @@ begin end; function TCATEngine.ZZMG(const s: string): string; -// ZZMG: microphone gain 0–70 (Thetis scale) — STUB: TX DSP not implemented +// ZZMG: усиление микрофона в дБ. Формат Thetis: SET — три цифры либо знак+две +// ('+10'/'-05'), GET всегда отдаёт знак+две. Значение уезжает в активный +// TX-профиль (MicGainDB), поэтому подрезаем под диапазон вкладки Transmit. +var v: Integer; begin - if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050' - else Result := CAT_ERROR; + if Length(s) = 0 then + Exit(SignedPad(EnsureRange(SafeGetMicGain, MIC_GAIN_MIN, MIC_GAIN_MAX), 2)); + // Три символа (`025`, `+10`, `-05`) либо четыре со знаком (`-050`) — ровно + // как у Thetis (nSet и nSet+1). Более короткие поля — ошибка формата, а не + // «маленькое значение»: молча крутить усиление по ним нельзя. + if (Length(s) < 3) or (Length(s) > 4) then Exit(CAT_ERROR); + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + if Assigned(FCtx.SetMicGain) then + FCtx.SetMicGain(EnsureRange(v, MIC_GAIN_MIN, MIC_GAIN_MAX)); + Result := ''; end; function TCATEngine.ZZML: string; @@ -1849,7 +2185,9 @@ function TCATEngine.ZZML: string; begin Result := 'LSB USB DSB CWL CWU FM AM SAM DIGUDIGLWFM DMR FMRAW'; end; function TCATEngine.ZZMN(const s: string): string; -// ZZMN: mode name — returns mode name for given mode number. +// ZZMN: имя режима по номеру. ⚠ Отклонение от Thetis: там ZZMN отдаёт пресеты +// DSP-фильтров (180 символов). Имён режимов в протоколе нет вовсе, а список +// режимов (ZZML) на эту команду опирается — оставляем как расширение EWSDR. // Индекс режима двузначный (MODE_MIN..MODE_MAX), но односимвольный запрос // продолжаем принимать — старые клиенты шлют одну цифру. var k: Integer; @@ -1869,22 +2207,27 @@ begin end; function TCATEngine.ZZMO(const s: string): string; -// ZZMO: monitor on/off — STUB: no TX monitor +// ZZMO: кнопка MON. У EWSDR монитор передачи — это self-monitor даунлинка +// (RX MUTE снят); слышно его, разумеется, только в дуплексе. Уровень — ZZTM. begin - if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' - else Result := CAT_ERROR; + Result := OnOff(s, FCtx.GetMonitorOn, FCtx.SetMonitorOn); end; function TCATEngine.ZZMV: string; -// ZZMV: mode value — returns current ewsdr mode index (MODE_MIN..MODE_MAX) -begin Result := Pad(SafeGetMode, 2); end; +// ZZMV: число записей в памяти каналов (read-only). +// (Текущий режим — это ZZMD; раньше ZZMV отдавал его индекс.) +var n: Integer; +begin + if Assigned(FCtx.GetMemoryCount) then n := FCtx.GetMemoryCount else n := 0; + Result := Pad(n, 3); +end; function TCATEngine.ZZMY: string; // ZZMY: mode Y — STUB begin Result := ''; end; function TCATEngine.ZZNA(const s: string): string; -// ZZNA: noise blanker A (NB1) on/off +// ZZNA: кнопка NB1 (у нас режимы NB нумерованы, 1 = первый бланкер). var v: Integer; begin if Length(s) = 1 then begin @@ -1912,17 +2255,17 @@ begin end; function TCATEngine.ZZNL(const s: string): string; -// ZZNL: NB level — STUB: NB level not configurable +// ZZNL: порог NB1 — STUB: порог бланкера не вынесен в настройки. begin if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050' else Result := CAT_ERROR; end; function TCATEngine.ZZNN(const s: string): string; -// ZZNN: notch (manual) on/off — STUB: no manual notch +// ZZNN: спектральный бланкер (SNB) RX1. +// (Раньше здесь стояла заглушка «ручной нотч», а SNB сидел в ZZNS.) begin - if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' - else Result := CAT_ERROR; + Result := OnOff(s, FCtx.GetSNBEnabled, FCtx.SetSNBEnabled); end; function TCATEngine.ZZNR(const s: string): string; @@ -1940,15 +2283,19 @@ begin end; function TCATEngine.ZZNS(const s: string): string; -// ZZNS: spectral noise blanker (SNB) on/off +// ZZNS: кнопка NR2 — у нас шумоподавитель один и режимный, NR2 = режим 2. +// Включение ставит режим 2, выключение — 0; прочие режимы читаются как '0'. +// (SNB переехал в ZZNN, где ему и место по протоколу.) +const NR2 = 2; 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; + if Length(s) = 0 then Exit(B2C(SafeGetNRMode = NR2)); + if (s <> '0') and (s <> '1') then Exit(CAT_ERROR); + if Assigned(FCtx.SetNRMode) then + begin + if s = '1' then FCtx.SetNRMode(NR2) + else if SafeGetNRMode = NR2 then FCtx.SetNRMode(0); + end; + Result := ''; end; function TCATEngine.ZZNT(const s: string): string; @@ -1963,11 +2310,15 @@ begin end; function TCATEngine.ZZOA(const s: string): string; -// ZZOA: RX1 output level — alias ZZAG -begin Result := ZZAG(s); end; +// ZZOA: антенна RX1 — STUB: порты AD936x в контроллере есть (SetAnt936x), но в +// CAT не проброшены (см. §3 doc/CAT_STATUS.md). +// (Громкость — это ZZAG; раньше ZZOA был ошибочно заалиашен на неё.) +begin + Result := OnOff(s, nil, nil); +end; function TCATEngine.ZZOB(const s: string): string; -// ZZOB: RX2 output level — STUB: no RX2 +// ZZOB: антенна RX2 — STUB: RX2 нет. begin if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '050' else Result := CAT_ERROR; @@ -1988,15 +2339,15 @@ begin end; function TCATEngine.ZZPD: string; -// ZZPD: power default — return nominal 100 -begin Result := '100'; end; +// ZZPD: центрировать пан дисплея — STUB: пан/зум в CAT не проброшены. +// (Раньше команда отдавала «номинальную мощность 100» — выдумка.) +begin Result := ''; end; function TCATEngine.ZZPO(const s: string): string; -// ZZPO: power on/off — STUB: always on when running +// ZZPO: кнопка Peak на дисплее — STUB: удержания пика в CAT нет. +// (Питание/состояние — это ZZPS; раньше ZZPO отвечал за него.) begin - if Length(s) = 1 then Result := '' - else if Length(s) = 0 then Result := B2C(SafeGetRunning) - else Result := CAT_ERROR; + Result := OnOff(s, nil, nil); end; function TCATEngine.ZZPS(const s: string): string; @@ -2008,8 +2359,9 @@ begin end; function TCATEngine.ZZQM: string; -// ZZQM: current mode query -begin Result := IntToStr(ModeToK(SafeGetMode)); end; +// ZZQM: частота из быстрой памяти (11 цифр) — STUB: быстрая память не заведена. +// (Текущий режим — это ZZMD; раньше ZZQM отдавал его.) +begin Result := Pad(0, 11); end; function TCATEngine.ZZQR: string; // ZZQR: quick recall — STUB @@ -2020,7 +2372,7 @@ function TCATEngine.ZZQS: string; begin Result := ''; end; function TCATEngine.ZZRA(const s: string): string; -// ZZRA: RIT A value — STUB: no RIT +// ZZRA: галка RTTY-офсета для VFO A — STUB: RTTY-движка нет. begin if Length(s) = 5 then Result := '' else if Length(s) = 0 then Result := '+0000' else Result := CAT_ERROR; @@ -2030,47 +2382,54 @@ function TCATEngine.ZZRC: string; begin Result := ''; end; // clear RIT — STUB function TCATEngine.ZZRD(const s: string): string; -// ZZRD: tune/RIT down +// ZZRD: убавить RIT — STUB: расстройки приёма в EWSDR нет. +// (Шаг настройки вниз — это ZZSA/ZZSD и кенвудовская RD.) begin - if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown; - Result := ''; + if (Length(s) = 0) or (Length(s) = 5) then Result := '' else Result := CAT_ERROR; end; function TCATEngine.ZZRL(const s: string): string; -// ZZRL: RF gain — STUB: no RF gain control +// ZZRL: частота офсета RTTY DIGL — STUB: RTTY-движка нет. begin if Length(s) = 3 then Result := '' else if Length(s) = 0 then Result := '100' else Result := CAT_ERROR; end; function TCATEngine.ZZRU(const s: string): string; -// ZZRU: tune/RIT up +// ZZRU: прибавить RIT — STUB: расстройки приёма в EWSDR нет. +// (Шаг настройки вверх — это ZZSB/ZZSU и кенвудовская RU.) begin - if Assigned(FCtx.DoTuneUp) then FCtx.DoTuneUp; - Result := ''; + if (Length(s) = 0) or (Length(s) = 5) then Result := '' else Result := CAT_ERROR; end; function TCATEngine.ZZRV: string; -// ZZRV: software version -begin Result := '001000'; end; // version 0.1.0 +// ZZRV: напряжение питания PA, формат Thetis «NN.N» (4 символа). +// (Версия ПО — это ZZVN; раньше ZZRV отдавал её.) +var v: Double; +begin + if Assigned(FCtx.GetSupplyVolts) then v := FCtx.GetSupplyVolts else v := -1; + if v < 0 then v := 0; // телеметрии нет — отвечаем нулём, как Thetis + if v > 99.9 then v := 99.9; + Result := Format('%04.1f', [v]); +end; function TCATEngine.ZZRX(const s: string): string; -// ZZRX: switch to receive +// ZZRX: шаговый аттенюатор RX1, дБ (00..31). +// (Переход на приём — это ZZTX0 и кенвудовская RX; раньше ZZRX делал его.) begin - if Assigned(FCtx.SetTransmitting) then FCtx.SetTransmitting(False); - if Length(s) = 0 then Result := B2C(not SafeGetTX) - else Result := ''; + Result := NumField(s, 2, 0, 31, FCtx.GetAtten, FCtx.SetAtten); end; function TCATEngine.ZZSA: string; -// ZZSA: VFO tune down (maps to step down) +// ZZSA: шаг настройки вниз. У Thetis шагает именно VFO A, у нас — активный VFO +// (отдельного «шага VFO A» в контроллере нет). begin if Assigned(FCtx.DoTuneDown) then FCtx.DoTuneDown; Result := ''; end; function TCATEngine.ZZSB: string; -// ZZSB: VFO tune up (maps to step up) +// ZZSB: шаг настройки вверх — см. оговорку в ZZSA. begin if Assigned(FCtx.DoTuneUp) then FCtx.DoTuneUp; Result := ''; @@ -2129,11 +2488,10 @@ begin end; function TCATEngine.ZZSR(const s: string): string; -// ZZSR: spectrum reference level (dBm, signed 4 chars e.g. "-020") +// ZZSR: кнопка Spur Reduction — STUB: такого блока в тракте нет. +// (Опорный уровень спектра — это ZZDP; раньше ZZSR отвечал за него.) 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; + Result := OnOff(s, nil, nil); end; function TCATEngine.ZZST(const s: string): string; @@ -2156,15 +2514,10 @@ begin end; function TCATEngine.ZZTA(const s: string): string; -// ZZTA: CTCSS on/off (1=on, 0=off) для FM-передачи. +// ZZTA: CTCSS вкл/выкл для FM-передачи. Значение строго 0/1 — иначе «CT9;» +// молча выключал бы тон вместо ответа об ошибке. begin - if Length(s) = 1 then begin - if Assigned(FCtx.SetCTCSSOn) then FCtx.SetCTCSSOn(s = '1'); - Result := ''; - end else if Length(s) = 0 then - Result := B2C(SafeGetCTCSSOn) - else - Result := CAT_ERROR; + Result := OnOff(s, FCtx.GetCTCSSOn, FCtx.SetCTCSSOn); end; function TCATEngine.ZZTB(const s: string): string; @@ -2180,63 +2533,53 @@ begin end; function TCATEngine.ZZTX(const s: string): string; -// ZZTX: TX on/off (0=RX, 1=TX, 2=TUNE) +// ZZTX: кнопка MOX — только 0 (приём) и 1 (передача). Никакого «2=TUNE» в +// протоколе нет (настройка — это ZZTU): на любом другом значении отвечаем +// ошибкой и НЕ поднимаем передачу. 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; + Result := OnOff(s, FCtx.GetTransmitting, FCtx.SetTransmitting); end; function TCATEngine.ZZUP(const s: string): string; -// ZZUP: xPA on/off — STUB: no external PA control +// ZZUP: внешний PA вкл/выкл — STUB: управления внешним усилителем нет. begin if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' else Result := CAT_ERROR; end; function TCATEngine.ZZUS: string; -// ZZUS: two-tone test — STUB: TX DSP not implemented -begin Result := ''; end; +// ZZUS: запустить одиночную калибровку PureSignal (write-only). +begin + if Assigned(FCtx.DoPSSingleCal) then FCtx.DoPSSingleCal; + Result := ''; +end; function TCATEngine.ZZUT(const s: string): string; -// ZZUT: TX update — STUB +// ZZUT: двухтональник (2TON). begin - if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' - else Result := CAT_ERROR; + Result := OnOff(s, FCtx.GetTwoTone, FCtx.SetTwoTone); end; function TCATEngine.ZZUX(const s: string): string; -// ZZUX: VFO A lock — STUB +// ZZUX: блокировка VFO A — STUB: у нас лок общий (SetVfoLock), раздельного нет. begin if Length(s) = 1 then Result := '' else if Length(s) = 0 then Result := '0' else Result := CAT_ERROR; end; function TCATEngine.ZZUY(const s: string): string; -// ZZUY: VFO B lock — STUB +// ZZUY: блокировка VFO B — STUB: см. ZZUX. begin Result := ZZUX(s); end; function TCATEngine.ZZVA(const s: string): string; -// ZZVA: swap VFO A↔B -var tmp: Double; +// ZZVA: включение VAC — STUB: виртуального аудиокабеля в EWSDR нет. +// (Обмен VFO — это ZZVS; раньше он жил здесь.) 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; + Result := OnOff(s, nil, nil); end; function TCATEngine.ZZVB(const s: string): string; -// ZZVB: copy VFO A to B — STUB: needs explicit operation +// ZZVB: усиление приёма в VAC — STUB: виртуального аудиокабеля нет. begin if Length(s) = 0 then begin if Assigned(FCtx.GetVfoA) and Assigned(FCtx.SetVfoB) then @@ -2246,19 +2589,39 @@ begin end; function TCATEngine.ZZVG(const s: string): string; -// ZZVG: volume/gain — alias ZZAG -begin Result := ZZAG(s); end; +// ZZVG: усиление VOX — STUB: VOX'а нет. +// (Громкость — это ZZAG; раньше ZZVG был ошибочно заалиашен на неё.) +begin + if Length(s) = 4 then Result := '' else if Length(s) = 0 then Result := '0000' + else Result := CAT_ERROR; +end; function TCATEngine.ZZVN: string; // ZZVN: software version string begin Result := 'EWSDR 0.1.0'; end; function TCATEngine.ZZVS(const s: string): string; -// ZZVS: VFO split — STUB -begin Result := ZZSP(s); end; +// ZZVS: операция над парой VFO (write-only). Аргумент — НОМЕР операции, а не +// флаг: 0 = A→B, 1 = B→A, 2 = обмен, 3 = IF→VFO. +// (Split — это ZZSP; раньше ZZVS был заалиашен на него.) +begin + if Length(s) <> 1 then Exit(CAT_ERROR); + case s[1] of + '0': if Assigned(FCtx.SetVfoB) then FCtx.SetVfoB(SafeGetVfoA); + '1': if Assigned(FCtx.SetVfoA) then FCtx.SetVfoA(SafeGetVfoB); + '2': if Assigned(FCtx.DoVfoSwap) then FCtx.DoVfoSwap; + // IF→VFO переносит расстройку приёма в частоту VFO. Расстройки у нас нет, + // переносить нечего — команду принимаем и ничего не делаем. + '3': ; + else + Exit(CAT_ERROR); + end; + Result := ''; +end; -// WA..WS: Waterfall settings — STUB: waterfall not controllable via CAT -// These are Thetis-specific waterfall display controls +// ZZWA..ZZWS: микшер Flex 5000 / FLEX-1500 (уровни и галки входов/выходов) — +// STUB: железо другого вендора, аналога нет. Водопад тут ни при чём: его +// уровни живут в ZZDN/ZZDO. function TCATEngine.ZZWA(const s: string): string; begin if Length(s) > 0 then Result := '' else Result := '0'; end; function TCATEngine.ZZWB(const s: string): string; @@ -2296,7 +2659,8 @@ 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 +// ZZXA: включение аудиоусилителя G2; ZZXC/ZZXF: сброс и значение XIT — +// STUB: усилителя не касаемся, расстройки передачи (XIT) в EWSDR нет. function TCATEngine.ZZXA(const s: string): string; begin if Length(s) > 0 then Result := '' else Result := '0'; end; function TCATEngine.ZZXC: string; @@ -2304,7 +2668,9 @@ 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 +// ZZYA/ZZYB: галки Direct I/Q и IQ Calibrate для VAC2 — STUB: VAC нет. +// ZZYC: отдельное усиление микрофона в FM — STUB: у нас оно одно на все режимы +// (ZZMG). ZZYR: переключатель RX1/RX2 — STUB: RX2 нет. function TCATEngine.ZZYA(const s: string): string; begin if Length(s) > 0 then Result := '' else Result := '0'; end; function TCATEngine.ZZYB(const s: string): string; @@ -2314,7 +2680,10 @@ 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) +// ZZZA: трип-состояние усилителя Ganymede; ZZZB, ZZZQ, ZZZR: внутренние +// служебные Thetis; ZZZM/ZZZV: строки модели и версии железа; ZZZT: зум на +// диапазон; ZZZW: обмен «колёс» VFO в midi2cat; ZZZO: быстрый split. +// Всё — STUB: либо чужое железо, либо в CAT не проброшено. function TCATEngine.ZZZA(const s: string): string; begin if Length(s) > 0 then Result := '' else Result := '0'; end; function TCATEngine.ZZZB: string; begin Result := ''; end; @@ -2395,9 +2764,112 @@ 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 +// ZZZZ: без описания у Thetis — STUB. begin Result := ''; end; function TCATEngine.ZZZO(const s: string): string; begin if Length(s) > 0 then Result := '' else Result := '0'; end; +{ ── TX-тракт и телеграф ──────────────────────────────────────────────────── } + +function TCATEngine.ZZCI(const s: string): string; +// ZZCI: иамбический манипулятор вкл/выкл. У нас режим кейера трёхпозиционный +// (прямой ключ / iambic A / iambic B) — включение выбирает B, как в Thetis по +// умолчанию, а если иамбик уже стоит, ранее выбранная буква сохраняется. +const KEYER_STRAIGHT = 0; KEYER_IAMBIC_B = 2; +begin + if Length(s) = 0 then Exit(B2C(SafeGetCWKeyerMode <> KEYER_STRAIGHT)); + if (s <> '0') and (s <> '1') then Exit(CAT_ERROR); + if Assigned(FCtx.SetCWKeyerMode) then + begin + if s = '0' then FCtx.SetCWKeyerMode(KEYER_STRAIGHT) + else if SafeGetCWKeyerMode = KEYER_STRAIGHT then FCtx.SetCWKeyerMode(KEYER_IAMBIC_B); + end; + Result := ''; +end; + +function TCATEngine.ZZCL(const s: string): string; +// ZZCL: тон телеграфа (он же сайдтон), Гц. +begin + Result := NumField(s, 4, CW_PITCH_MIN, CW_PITCH_MAX, FCtx.GetCWPitch, FCtx.SetCWPitch); +end; + +function TCATEngine.ZZCM(const s: string): string; +// ZZCM: у Thetis это «CW Monitor DISABLE» — значение инвертировано к сайдтону. +begin + if Length(s) = 0 then Exit(B2C(not SafeGetBool(FCtx.GetCWMonitor))); + if (s <> '0') and (s <> '1') then Exit(CAT_ERROR); + if Assigned(FCtx.SetCWMonitor) then FCtx.SetCWMonitor(s = '0'); + Result := ''; +end; + +function TCATEngine.ZZET(const s: string): string; +// ZZET: кнопка TXEQ. +begin + Result := OnOff(s, FCtx.GetTXEQOn, FCtx.SetTXEQOn); +end; + +function TCATEngine.ZZLI(const s: string): string; +// ZZLI: кнопка PS-A (автокалибровка PureSignal). +begin + Result := OnOff(s, FCtx.GetPureSignal, FCtx.SetPureSignal); +end; + +function TCATEngine.ZZPK(const s: string): string; +// ZZPK: речевой компрессор (WDSP COMP) вкл/выкл. Пара к ZZPL. +begin + Result := OnOff(s, FCtx.GetCompOn, FCtx.SetCompOn); +end; + +function TCATEngine.ZZPL(const s: string): string; +// ZZPL: усиление речевого компрессора, дБ. +begin + Result := NumField(s, 2, 0, COMP_GAIN_MAX, FCtx.GetCompGain, FCtx.SetCompGain); +end; + +function TCATEngine.ZZTH(const s: string): string; +// ZZTH: верхняя кромка TX-фильтра, Гц. +begin + Result := NumField(s, 5, TX_FILT_HI_MIN, TX_FILT_HI_MAX, + FCtx.GetTXFilterHigh, FCtx.SetTXFilterHigh); +end; + +function TCATEngine.ZZTL(const s: string): string; +// ZZTL: нижняя кромка TX-фильтра, Гц. +begin + Result := NumField(s, 4, TX_FILT_LO_MIN, TX_FILT_LO_MAX, + FCtx.GetTXFilterLow, FCtx.SetTXFilterLow); +end; + +function TCATEngine.ZZTM(const s: string): string; +// ZZTM: громкость монитора передачи, 0..100. +begin + Result := NumField(s, 3, 0, 100, FCtx.GetMonitorLevel, FCtx.SetMonitorLevel); +end; + +function TCATEngine.ZZTO(const s: string): string; +// ZZTO: мощность настройки (уровень TUN), % от полной шкалы Drive. +begin + Result := NumField(s, 3, 0, 100, FCtx.GetTUNLevel, FCtx.SetTUNLevel); +end; + +function TCATEngine.ZZTP(const s: string): string; +// ZZTP: активный TX-профиль по индексу. Индекса вне списка нет — это ошибка +// выполнения, а не подрезка: клиент выбирает из того, что ему отдал ZZTP;. +var v, Cnt: Integer; +begin + if Length(s) = 0 then Exit(Pad(Max(0, SafeGetTXProfile), 2)); + if Length(s) <> 2 then Exit(CAT_ERROR); + if not TryStrToInt(s, v) then Exit(CAT_ERROR); + if Assigned(FCtx.GetTXProfileCount) then Cnt := FCtx.GetTXProfileCount else Cnt := 0; + if (v < 0) or (v >= Cnt) then Exit(CAT_ERROR); + if Assigned(FCtx.SetTXProfile) then FCtx.SetTXProfile(v); + Result := ''; +end; + +function TCATEngine.ZZTU(const s: string): string; +// ZZTU: кнопка TUN. +begin + Result := OnOff(s, FCtx.GetTuning, FCtx.SetTuning); +end; + end. diff --git a/CATSerial.pas b/CATSerial.pas index 3eca08a..4385321 100644 --- a/CATSerial.pas +++ b/CATSerial.pas @@ -63,7 +63,6 @@ type FHandle: TSerialHandle; FBuf: string; procedure ProcessBuffer; - function TryOpenPort: Boolean; protected procedure Execute; override; public @@ -79,6 +78,9 @@ type FHandle: TSerialHandle; FLock: TCriticalSection; FActive: Boolean; + FLastError: string; + function OpenPort: Boolean; // синхронно, из Start — чтобы отказ был виден сразу + procedure ClosePort; public constructor Create(AEngine: TCATEngine; const ACfg: TCATSerialConfig); destructor Destroy; override; @@ -89,6 +91,9 @@ type property Config: TCATSerialConfig read FConfig; property Engine: TCATEngine read FEngine; property Active: Boolean read FActive; + // Причина, по которой порт не поднялся (пусто, если поднялся). Раньше отказ + // SerOpen нигде не оседал, а порт продолжал числиться активным. + property LastError: string read FLastError; end; { TCATSerialManager — manages up to 4 serial ports } @@ -120,40 +125,6 @@ begin inherited Create(True); end; -function TCATSerialThread.TryOpenPort: Boolean; - {$IFNDEF CAT_SERIAL_STUB} -var - cfg: TCATSerialConfig; - h: TSerialHandle; - par: TParityType; - {$ENDIF} -begin - Result := False; - {$IFDEF CAT_SERIAL_STUB} - Exit; - {$ELSE} - 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 := Serial.NoneParity; - cspOdd: par := Serial.OddParity; - cspEven: par := Serial.EvenParity; - else par := Serial.NoneParity; - end; - - SerSetParams(h, cfg.BaudRate, cfg.DataBits, par, cfg.StopBits, []); - FHandle := h; - FOwner.Handle := h; - Result := True; - {$ENDIF} -end; - procedure TCATSerialThread.ProcessBuffer; var tpos: Integer; @@ -173,6 +144,8 @@ begin end; procedure TCATSerialThread.Execute; +// Порт уже открыт владельцем (TCATSerialPort.Start) — поток только читает. +// Закрывает тоже владелец, в Stop, после WaitFor. var ch: Byte; n: LongInt; @@ -180,22 +153,13 @@ begin {$IFDEF CAT_SERIAL_STUB} Exit; {$ELSE} - 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; + FHandle := FOwner.Handle; + 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; - finally - SerClose(FHandle); - {$IFDEF MSWINDOWS} - FOwner.Handle := INVALID_HANDLE_VALUE; - {$ELSE} - FOwner.Handle := -1; - {$ENDIF} end; {$ENDIF} end; @@ -223,9 +187,61 @@ begin inherited; end; +function TCATSerialPort.OpenPort: Boolean; + {$IFNDEF CAT_SERIAL_STUB} +var + h: TSerialHandle; + par: TParityType; + {$ENDIF} +begin + Result := False; + {$IFDEF CAT_SERIAL_STUB} + FLastError := 'serial support not built on this platform'; + {$ELSE} + h := SerOpen(FConfig.PortName); + {$IFDEF MSWINDOWS} + if h = TSerialHandle(INVALID_HANDLE_VALUE) then + {$ELSE} + if h < 0 then + {$ENDIF} + begin + FLastError := 'не удалось открыть ' + FConfig.PortName; + Exit; + end; + + case FConfig.Parity of + cspNone: par := Serial.NoneParity; + cspOdd: par := Serial.OddParity; + cspEven: par := Serial.EvenParity; + else par := Serial.NoneParity; + end; + + SerSetParams(h, FConfig.BaudRate, FConfig.DataBits, par, FConfig.StopBits, []); + FHandle := h; + FLastError := ''; + Result := True; + {$ENDIF} +end; + +procedure TCATSerialPort.ClosePort; +begin + {$IFNDEF CAT_SERIAL_STUB} + {$IFDEF MSWINDOWS} + if FHandle <> INVALID_HANDLE_VALUE then SerClose(FHandle); + FHandle := INVALID_HANDLE_VALUE; + {$ELSE} + if FHandle >= 0 then SerClose(FHandle); + FHandle := -1; + {$ENDIF} + {$ENDIF} +end; + procedure TCATSerialPort.Start; +// Порт открываем ЗДЕСЬ, а не в потоке: иначе отказ SerOpen оставался внутри +// потока, а порт всё это время числился активным — и в ActiveCount, и в UI. begin if FActive or not FConfig.Enabled then Exit; + if not OpenPort then Exit; // FActive остаётся False, причина — в LastError FActive := True; FThread := TCATSerialThread.Create(Self); FThread.Start; @@ -240,6 +256,7 @@ begin FThread.WaitFor; FreeAndNil(FThread); end; + ClosePort; end; procedure TCATSerialPort.SendStr(const S: string); @@ -294,7 +311,9 @@ begin for i := 0 to cnt-1 do begin FPorts[i] := TCATSerialPort.Create(FEngine, Configs[i]); if Configs[i].Enabled then FPorts[i].Start; - if Configs[i].Enabled and Configs[i].Andromeda and (FAndromedaPort = nil) then + // Панель назначаем только на РЕАЛЬНО поднявшийся порт: раньше хватало галки + // в настройках, и HasAndromeda рапортовал о панели, которой нет. + if FPorts[i].Active and Configs[i].Andromeda and (FAndromedaPort = nil) then FAndromedaPort := FPorts[i]; end; end; diff --git a/CATTcp.pas b/CATTcp.pas index 640a223..0f2a2cc 100644 --- a/CATTcp.pas +++ b/CATTcp.pas @@ -161,22 +161,32 @@ begin end; procedure TCATTcpClientThread.SendStr(const S: string); +// TCP не обязан отдавать весь буфер за один send: длинный ответ (IF, ZZEB, +// список режимов) мог уехать обрезанным — и молча, потому что усечение здесь +// не ошибка. Дописываем остаток, пока он есть. var - buf: AnsiString; - n: Integer; + buf: AnsiString; + sent, 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; + sent := 0; + while sent < Length(buf) do + begin + {$IFDEF MSWINDOWS} + n := send(FSocket, buf[sent + 1], Length(buf) - sent, 0); + if n = SOCKET_ERROR then n := -1; + {$ELSE} + n := fpSend(FSocket, @buf[sent + 1], Length(buf) - sent, 0); + {$ENDIF} + if n <= 0 then + begin + FMarkDelete := True; + Terminate; + Exit; + end; + Inc(sent, n); end; end; diff --git a/MainForm.pas b/MainForm.pas index 9857dc4..6a2dc38 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -8520,64 +8520,23 @@ begin end; procedure TMainForm.OnTXSettingsChange(const T: TTXSettings); -// Любая правка в Settings→Transmit летит сюда: обновляем FController.FTXSettings, -// перешиваем WDSP TX-цепь и (если поменялись mic-биты) перепосылаем DUC Specific. -var - OldMicByte, NewMicByte: Byte; - OldAtt, OldTUNLevel, OldTUNFreq: Integer; - GridChanged: Boolean; +// Любая правка в Settings→Transmit летит сюда. Применение записи (WDSP, DUC +// Specific, живой TUN, профиль, персист) — в контроллере: SetTXSettings, та же +// дверь, что у CAT и web. Форме остаётся рендер. +// Notify=False — иначе rfTXProfile перезагрузил бы вкладку Transmit прямо под +// руками у того, кто её сейчас правит. +var GridChanged: Boolean; begin - OldMicByte := FController.BuildMicLineSelectByte; - OldAtt := FController.FTXSettings.AttOnTX; - OldTUNLevel := FController.FTXSettings.TUNLevel; - OldTUNFreq := FController.FTXSettings.TUNFreq; GridChanged := (FController.FTXSpecRefLevel <> T.TXSpecRefLevel) or (FController.FTXSpecRange <> T.TXSpecRange) or (FController.FTXSpecGridStep <> T.TXSpecGridStep); - FController.FTXSettings := T; - NewMicByte := FController.BuildMicLineSelectByte; - // TX-grid в момент правки — обновляем рабочие поля и (если на передаче) пушим в FSpecView - FController.FTXSpecRefLevel := FController.FTXSettings.TXSpecRefLevel; - FController.FTXSpecRange := FController.FTXSettings.TXSpecRange; - if FController.FTXSettings.TXSpecGridStep > 0 then - FController.FTXSpecGridStep := FController.FTXSettings.TXSpecGridStep; + FController.SetTXSettings(T, False); if GridChanged and FController.FTransmitting then ApplySpecViewGridFromState; - FController.ApplyTXSettingsToDSP; // Low/High из вкладки Transmit — та же полоска фильтра на спектре: кромки // берём уже пересчитанными движком (знак боковой) и на передаче перерисовываем. PushFilterEdgesToView; if FController.FTransmitting then FSpectrumDirty := True; - if (OldMicByte <> NewMicByte) or (OldAtt <> FController.FTXSettings.AttOnTX) then - FController.SendDUCSpecificFromSettings; - // TUN-параметры live: если идёт tune — обновить тон/drive, иначе просто запомнить. - if FController.FTuning then - begin - if (OldTUNFreq <> FController.FTXSettings.TUNFreq) and FController.FWDSPReady then - FController.FDSPEngine.SetTXTone(True, FController.FTXSettings.TUNFreq, 1.0); - if OldTUNLevel <> FController.FTXSettings.TUNLevel then - begin - FController.FDriveLevel := CalcDriveByte; - if FController.FWDSPReady then - FController.FDSPEngine.SetDriveLevel(FController.FTXSettings.TUNLevel / 100.0); - if FController.FNetwork.Connected and FController.FNetwork.Running then - begin - FController.FNetwork.UpdateState(XvtrTranslate(FController.FCenterFreq), FController.TXTuneFreqHz, FController.FDriveLevel, - FController.FTransmitting, True, True, FController.FTuning, FController.FTwoTone); - FController.FNetwork.SendFullHP; - end; - end; - end; - // Правка «звуковых» полей — это правка активного профиля: отдельной кнопки - // «сохранить» нет, профиль ведётся за живым состоянием (см. TTXProfile). - FController.StoreActiveTXProfile; - // Сохраняем сразу — настройки per-device, попадают в hpsdr_settings.json - if FController.FDevConnected then - begin - FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings); - FController.FSettings.SaveTXProfiles(FController.FDevMAC, FController.FTXProfiles); - FController.FSettings.Save; - end; end; procedure TMainForm.OnCWSettingsChange(const C: TCWSettings); diff --git a/RadioController.pas b/RadioController.pas index 8513296..aa84084 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -691,6 +691,11 @@ type function TXProfileCount: Integer; function TXProfileName(Idx: Integer): string; function ActiveTXProfile: Integer; + // Единая дверь для правки TX-настроек снаружи (вкладка Transmit, CAT, web): + // применяет запись к движку/железу, дописывает активный профиль и персистит. + // Notify=False — вызывающий сам отрисует изменения (так делает MainForm, + // иначе rfTXProfile перезагрузил бы форму настроек прямо под руками). + procedure SetTXSettings(const T: TTXSettings; Notify: Boolean = True); // Auto=True — переключение сделала автоматика привязок; ручной выбор // (дропдаун, список настроек) дополнительно ЗАПОМИНАЕТСЯ за текущей парой // «источник + модуляция» (BindTXProfileToCurrentMode). @@ -787,6 +792,9 @@ type // Громкость / mute / drive / аттенюатор procedure SetVolume(V: Integer); procedure VolumeBy(Delta: Integer); + // Громкость self-monitor'а на передаче отдельно от RX-громкости: слайдер + // правит её только когда MonitoringTX, а CAT (ZZTM) — в любой момент. + procedure SetTXMonVolume(V: Integer); // True — сейчас звучит self-monitor даунлинка на передаче (TX + DUP + RX MUTE // off). В этом контексте SetVolume/слайдер правят FTxMonVolume, иначе FVolume. function MonitoringTX: Boolean; @@ -4683,6 +4691,14 @@ end; procedure TRadioController.VolumeBy(Delta: Integer); begin SetVolume(ActiveVolume + Delta); end; +procedure TRadioController.SetTXMonVolume(V: Integer); +// Адресно правит громкость self-monitor'а. Слайдер добирается до неё только на +// передаче (SetVolume через ActiveVolume), CAT-клиенту такой контекст не нужен. +begin + FTxMonVolume := EnsureRange(V, 0, 100); + if MonitoringTX then ApplyActiveVolume; +end; + procedure TRadioController.SetMute(On_: Boolean); begin FMuted := On_; @@ -5531,6 +5547,59 @@ begin FSettings.Save; end; +procedure TRadioController.SetTXSettings(const T: TTXSettings; Notify: Boolean); +// Одна дверь для правки TX-настроек — по образцу SetCWSettings. Клиенту +// (вкладка Transmit, CAT, web) достаточно отдать запись целиком: применение к +// WDSP, перепосыл DUC Specific при смене mic-битов/ATT, живое обновление TUN, +// запись в активный профиль и персист — здесь. +var + OldMicByte: Byte; + OldAtt, OldTUNLevel, OldTUNFreq: Integer; +begin + OldMicByte := BuildMicLineSelectByte; + OldAtt := FTXSettings.AttOnTX; + OldTUNLevel := FTXSettings.TUNLevel; + OldTUNFreq := FTXSettings.TUNFreq; + FTXSettings := T; + // TX-сетка спектра — рабочие поля рендера; их перерисовку заказывает вызывающий. + FTXSpecRefLevel := FTXSettings.TXSpecRefLevel; + FTXSpecRange := FTXSettings.TXSpecRange; + if FTXSettings.TXSpecGridStep > 0 then FTXSpecGridStep := FTXSettings.TXSpecGridStep; + ApplyTXSettingsToDSP; + // Маршрутизация микрофона и ATT on TX живут в железе (DUC Specific), WDSP тут + // ни при чём — пакет перепосылаем только когда байты реально поменялись. + if (BuildMicLineSelectByte <> OldMicByte) or (OldAtt <> FTXSettings.AttOnTX) then + SendDUCSpecificFromSettings; + // TUN-параметры live: идёт настройка — обновить тон/уровень, иначе просто запомнить. + if FTuning then + begin + if (OldTUNFreq <> FTXSettings.TUNFreq) and FWDSPReady then + FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0); + if OldTUNLevel <> FTXSettings.TUNLevel then + begin + FDriveLevel := CalcDriveByte; + if FWDSPReady then + FDSPEngine.SetDriveLevel(EnsureRange(FTXSettings.TUNLevel, 0, 100) / 100.0); + if FNetwork.Connected and FNetwork.Running then + begin + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), TXTuneFreqHz, FDriveLevel, + FTransmitting, True, True, FTuning, FTwoTone); + FNetwork.SendFullHP; + end; + end; + end; + // Правка «звуковых» полей — это правка активного профиля: отдельной кнопки + // «сохранить» нет, профиль ведётся за живым состоянием (см. TTXProfile). + StoreActiveTXProfile; + if FDevConnected and Assigned(FSettings) then + begin + FSettings.SaveTX(FDevMAC, FTXSettings); + FSettings.SaveTXProfiles(FDevMAC, FTXProfiles); + FSettings.Save; + end; + if Notify then Changed(rfTXProfile); +end; + procedure TRadioController.StoreActiveTXProfile(Persist: Boolean = False); // Живое состояние → активный профиль. Зовётся на каждой правке TX-настроек и // на смене drive; Persist=False не трогает диск (drive ездит слайдером). diff --git a/doc/CAT_STATUS.md b/doc/CAT_STATUS.md index ec2c214..44c0c75 100644 --- a/doc/CAT_STATUS.md +++ b/doc/CAT_STATUS.md @@ -1,7 +1,7 @@ # CAT-система EWSDR — статус реализации -Ветка разработки: `feature/cat-commands-expansion`. -Дата последнего обновления: 2026-06-26. +Ветка разработки: `feature/cat-tx-cw`. +Дата последнего обновления: 2026-08-17. Документ описывает текущее состояние CAT-подсистемы EWSDR: архитектуру, полный перечень реализованных команд, осознанные заглушки и план дальнейших @@ -20,9 +20,9 @@ CAT-транспорты ──► TCATEngine (парсер протокола) | Файл | Назначение | |---|---| -| `CATEngine.pas` (~2270 строк) | Движок протокола: разбор `;`, формирование ответов, реализации всех команд Kenwood + `ZZ*`. Зависит только от RTL. | -| `CATAdapter.pas` (~445 строк) | Мост к `TRadioController`. Строит `TCATContext`: геттеры читают поля контроллера напрямую (CAT-поток, read-only), сеттеры/команды маршалятся в поток контроллера через `FController.Invoke`. Владеет движком + транспортами. | -| `CATSerial.pas` (~313 строк) | Менеджер до 4 последовательных портов (виртуальные COM-пары для rigctld/N1MM/WSJT-X). | +| `CATEngine.pas` (~2740 строк) | Движок протокола: разбор `;`, формирование ответов, реализации всех команд Kenwood + `ZZ*`. Зависит только от RTL. | +| `CATAdapter.pas` (~910 строк) | Мост к `TRadioController`. Строит `TCATContext`: геттеры читают поля контроллера напрямую (CAT-поток, read-only), сеттеры/команды маршалятся в поток контроллера через `FController.Invoke`. Владеет движком + транспортами. | +| `CATSerial.pas` (~330 строк) | Менеджер до 4 последовательных портов (виртуальные COM-пары для rigctld/N1MM/WSJT-X). | | `CATTcp.pas` (~507 строк) | TCP-сервер (Thetis-совместимый), управление клиентами по GUID (`ZZGA`/`ZZGR`). | ### 1.1 Принципы @@ -51,12 +51,28 @@ CAT-транспорты ──► TCATEngine (парсер протокола) | | Кол-во | |---|---| | Всего `ZZ*` в Thetis | 315 | -| Реально реализовано в EWSDR | **98** | -| Заглушки (отвечают валидным dummy, функции нет) | ~211 | -| Совсем не обрабатываются (вернут `?;`) | 7 | +| Реально реализовано в EWSDR | **~120** | +| Заглушки (отвечают валидным dummy, функции нет) | ~190 | +| Совсем не обрабатываются (вернут `?;`) | 5 | -Большинство заглушек закрыть **нечем** — в EWSDR физически отсутствует -соответствующий тракт (см. §4). +Подписи всех 315 команд сверены с `CATCommands.cs` (см. §2.5) — комментарию у +заглушки теперь можно верить. Оставшиеся заглушки в большинстве закрыть +**нечем**: в EWSDR физически отсутствует соответствующий тракт (см. §4). + +### 1.3 Осознанные отклонения от Thetis + +Три места, где EWSDR намеренно расходится с эталоном. Больше таких нет. + +| Команда | У Thetis | У нас | Почему | +|---|---|---|---| +| `ZZBS` | код диапазона, 3 символа (`160`/`040`/`WWV`) | индекс диапазона, 2 цифры | формат заложен давно, на нём уже сидят клиенты | +| `ZZMN` | пресеты DSP-фильтров, 180 символов | имя режима по номеру | имён режимов в протоколе нет вовсе, а `ZZML` опирается на эту команду | +| `ZZST` | размер шага настройки (read-only) | индекс шага FM (00..03) | глобального шага у EWSDR нет, слот иначе мёртв; у шага FM своей команды в протоколе нет | + +Плюс одно ограничение диапазона, не формата: **`ZZCD`** (hang-time break-in) +эталон принимает в 150..5000 мс, у нас потолок 2000 — столько же стоит в +настройках телеграфа и уходит в поле `HangDelay` пакета DUC Specific. Значения +выше подрезаются, как и всё прочее у Thetis. --- @@ -89,13 +105,14 @@ CAT-транспорты ──► TCATEngine (парсер протокола) ### 2.2 Реализованные расширенные `ZZ*` (выборочно, наиболее значимые) -VFO/режим/фильтр: `ZZFA ZZFB ZZFI ZZFL ZZFH ZZFM ZZMD ZZFT ZZSP`, -диапазоны: `ZZBS ZZBU ZZBD ZZBA ZZBI ZZBM`, -AGC/громкость/мощность: `ZZGT ZZAG ZZPC ZZPD ZZAR`, -шумодав/нотч: `ZZNA ZZNB ZZNR ZZNS ZZNT ZZNL ZZNN`, +VFO/режим/фильтр: `ZZFA ZZFB ZZFI ZZFL ZZFH ZZFM ZZMD ZZFT ZZSP ZZVS`, +диапазоны: `ZZBS ZZBU ZZBD`, +AGC/громкость/мощность/аттенюатор: `ZZGT ZZAG ZZMA ZZPC ZZAR ZZRX`, +шумодав/нотч: `ZZNA ZZNB ZZNR ZZNS ZZNT ZZNN`, FM: `ZZSQ ZZSO ZZTA ZZTB ZZST`, CTUN: `ZZCN`, -S-метр/телеметрия: `ZZSM`, шаги VFO: `ZZAU ZZBP ZZSU ZZSA ZZSB`, -передача: `ZZTX ZZRX`, сервис: `ZZID ZZRV ZZGA ZZGR`. +S-метр/телеметрия: `ZZSM ZZRV`, память: `ZZMV`, +шаги VFO: `ZZAU ZZBP ZZBM ZZSU ZZSA ZZSB`, +передача: `ZZTX`, сервис: `ZZID ZZGA ZZGR`. ### 2.3 Сделано в ветке `feature/cat-commands-expansion` @@ -142,6 +159,133 @@ S-метр/телеметрия: `ZZSM`, шаги VFO: `ZZAU ZZBP ZZSU ZZSA ZZSB **Новые callback'и в `TCATContext`** (этой ветки): split, squelch on/level, CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN. +### 2.4 Сделано в ветке `feature/cat-tx-cw` + +К этому моменту в EWSDR появились TX-профили со всей «звуковой» цепью и +телеграф (кейер, сайдтон, break-in) — команды, которые §4 когда-то объявил +незакрываемыми, стали рабочими. + +**TX-тракт** (правки уезжают в активный TX-профиль): + +| Команда | Что | Маппинг | +|---|---|---| +| `MG` / `ZZMG` | усиление микрофона | `MicGainDB`; `MG` — шкала Kenwood 100:70, `ZZMG` — дБ со знаком | +| `MO` / `ZZMO` | кнопка MON | self-monitor даунлинка: `SetRxMuteOnTx` (инверсия) | +| `ZZTM` | громкость монитора | `SetTXMonVolume` (новый метод контроллера) | +| `ZZTL` / `ZZTH` | кромки TX-фильтра, Гц | `FilterLow` / `FilterHigh` | +| `PR` / `ZZPK` | речевой компрессор вкл/выкл | `CompressorOn` | +| `ZZPL` | усиление компрессора, дБ | `CompressorGain` (0..20) | +| `ZZET` | кнопка TXEQ | `EQOn` | +| `ZZEB` | значения TX-эквалайзера | `EQNumBands` + `EQGains` (формат Thetis, 36 символов) | +| `ZZTO` | мощность настройки | `TUNLevel` | +| `ZZTU` | кнопка TUN | `SetTune` | +| `ZZUT` | двухтональник 2TON | `SetTwoTone` | +| `ZZLI` | кнопка PS-A | `SetPureSignal` | +| `ZZUS` | одиночная калибровка PS | `PSSingleCal` (write-only) | +| `ZZTP` | активный TX-профиль | `SelectTXProfile` / `TXProfileCount` | +| `ZZFD` | девиация передачи (2.5/5 кГц) | `TTXSettings.FMDeviation` | + +**Телеграф** (раньше были проброшены только `KS`/`KY`/`ZZKM`/`ZZKS`/`ZZKY`): + +| Команда | Что | Маппинг (`TCWSettings`) | +|---|---|---| +| `ZZCS` | скорость кейера, WPM (2 разряда) | `.Speed` — та же величина, что у `KS` | +| `ZZCL` | тон приёма/сайдтона, Гц | `.Pitch` | +| `ZZCI` | иамбик вкл/выкл | `.KeyerMode` (вкл выбирает iambic B, если стоял прямой ключ) | +| `ZZCB` | break-in прошивки | `.BreakIn` | +| `ZZCD` | hang-time break-in, мс | `.HangTimeMS` | +| `ZZCM` | сайдтон (у Thetis — «monitor **disable**», значение инвертировано) | `.SidetoneSW` или `.SidetoneHW` — тот, что отвечает за текущий источник манипуляции | + +**Новое в контроллере.** Правка TX-настроек получила одну публичную дверь — +`SetTXSettings(const T; Notify)`, по образцу `SetCWSettings`: применение к WDSP, +перепосыл DUC Specific, живой TUN, запись в активный профиль и персист теперь +там, а не в обработчике MainForm (тот делегирует с `Notify=False`, иначе +`rfTXProfile` перезагрузил бы вкладку Transmit под руками у правящего). +Добавлен `SetTXMonVolume` — громкость self-monitor'а адресно, вне TX-контекста. + +**Новое в движке.** Хелперы `OnOff` и `NumField` — разбор двухпозиционных и +числовых полей в одном месте, чтобы тела команд остались про смысл. Границы +значений (`MIC_GAIN_MIN`… в шапке юнита) повторяют контролы вкладок Transmit и +CW: по CAT прилетает что угодно, а контроллер принимает запись целиком и уже не +знает, кто её правил. + +> Команды слайсов (`CATSliceEndpoint`) этих callback'ов не получают: TX-тракт +> один на радио, а профиль слайса переключается своим путём +> (`SetSliceTXProfile`). Все новые команды nil-безопасны и на слайс-порту +> отвечают дефолтом. + +### 2.5 Ревизия алиасов и подписей + +Подписи писались по буквам кода, а не по Thetis, и врали примерно в 150 +местах. Хуже: часть **работающих** команд была привязана не к своей функции — +внешний софт получал осмысленный, но неверный ответ, что хуже честной +заглушки. Всё сверено с `CATCommands.cs` и исправлено. + +**Команда делала не своё дело — теперь делает своё:** + +| Команда | Было | Стало | +|---|---|---| +| `ZZMA` | режим (алиас `ZZMD`) | кнопка **MUT** → `SetMute` | +| `ZZRX` | переход на приём | **шаговый аттенюатор RX1** 0..31 дБ → `SetAtten` | +| `ZZRV` | версия ПО | **напряжение питания** PA (телеметрия) | +| `ZZMV` | индекс текущего режима | **число записей** памяти каналов | +| `ZZNN` | заглушка «ручной нотч» | **SNB** (спектральный бланкер) | +| `ZZNS` | SNB | кнопка **NR2** (режим NR 2) | +| `ZZVS` | алиас `ZZSP` (split) | **операции с парой VFO**: `0`=A→B, `1`=B→A, `2`=обмен, `3`=IF→VFO | +| `ZZBM` | режим (алиас `ZZMD`) | **VFO B вниз** на nn шагов (пара к `ZZBP`) | +| `ZZFI` | алиас `ZZBI` | сам **индекс фильтра** | +| `ZZBS` | алиас `ZZAC` | сам **выбор диапазона** | +| `ZZKM` | режим кейера | запуск **CW-макроса** (ячейка памяти сообщений) | +| `ZZFT` | всегда VFO A | **TX-частота** с учётом split | +| `ZZTX` | любое значение ≠ `0` поднимало передачу («2=TUNE» — выдумка) | строго `0`/`1`, иначе `?;` и передача НЕ включается (настройка — `ZZTU`) | + +**Ошибочные алиасы сняты — теперь честные заглушки** (в скобках — где эта +функция живёт на самом деле): `ZZAA`, `ZZVG` (громкость — `ZZAG`) · `ZZOA` +(антенна RX1) · `ZZAP` (мощность — `ZZPC`) · `ZZDN` (мощность — `ZZPC`; сам +`ZZDN` = низ водопада) · `ZZAC` (диапазон — `ZZBS`) · `ZZBI` (фильтр — `ZZFI`) +· `ZZMB` (режим — `ZZMD`) · `ZZVA` (обмен VFO — `ZZVS`) · `ZZPD`, `ZZPO` +(мощность и питание — `ZZPC`/`ZZPS`) · `ZZQM` (режим — `ZZMD`) · `ZZSR` +(опорный уровень спектра) · `ZZRD`/`ZZRU` (RIT; шаг настройки — `ZZSA`/`ZZSB`). + +> Что заметят внешние клиенты: `ZZRX;` больше не уводит в приём (для этого есть +> `ZZTX0;` и кенвудовская `RX;`), `ZZVA;` больше не меняет VFO местами (теперь +> `ZZVS0;`), `ZZKM` не выставляет режим кейера (иамбик вкл/выкл — `ZZCI`), +> `RD`/`RU` больше не перестраивают VFO (шаг — `UP`/`DN`). +> Во всех случаях старое поведение было отсебятиной. + +**Кенвудовские команды.** Сверены отдельно: все 40 команд, реализованных в +Thetis, у нас есть, ширины полей совпадают с `CATStructs.xml`. Исправлено: + +| Команда | Что было не так | +|---|---| +| `SM` | ответ был 4 цифры вместо **5** (`SM0015;` → `SM00019;`) — парсеры логгеров ждут пять; заодно принимаем селектор `2`, как шлёт Thetis | +| `RD` / `RU` | перестраивали VFO, хотя это **RIT** (его у нас нет) — стали заглушками, принимающими 5-значный аргумент | +| `KY` | поле текста у Kenwood фиксированной ширины и добито пробелами — хвост уходил в эфир словесными паузами, теперь срезается | +| `IF` | в комментарии значилось «37 байт», реальная и правильная длина ответа — **35** | +| `SH` / `SL` | обе крутят один и тот же индекс пресета; раздельных «сторон» у главного приёмника нет — оговорка добавлена в код | +| `CT` | принимала любой символ: `CT9;` молча ГАСИЛ тон вместо ответа `?;`. Теперь обёртка над `ZZTA`, как в эталоне, и сама `ZZTA` проверяет значение строго | +| `OF` / `OS` | несли реализацию сами, а `ZZOT`/`ZZOS` были заглушками — клиент Thetis обращается как раз к `ZZ*` и не получал ничего. Реализация переехала в `ZZOT`/`ZZOS`, кенвудовские стали обёртками. Заодно `OF` с нечисловым полем больше не обнуляет сдвиг молча | + +**Разбор и транспорты.** Отдельный проход по валидации и вводу-выводу: + +| Где | Что было не так | +|---|---| +| `TCATEngine.Parse` | команды без параметров не проверяли суффикс: `TXanything;` доходил до `CmdTX` и **поднимал передачу**; так же вели себя `RX UP DN BD BU QI RC ID IF`. Эталон отбраковывает лишний суффикс в парсере, по таблице ширин; у нас таблицы нет — список безаргументных команд теперь в `IsParamless` | +| `ZZFL` / `ZZFH` | принимали поле любой длины от 4 символов и гнали его через `StrToIntDef`: `ZZFLabcd;` молча схлопывал кромку в ноль. Поле фиксированное — ровно 5 символов со знаком, разбор строгий | +| `KY` / `ZZKY` | текст не ограничивался; поле у Kenwood фиксированное, 25 символов. Длиннее — `?;`: очередь передачи не должна расти произвольно, иначе один пакет уводит станцию в эфир на неопределённое время | +| `CATTcp.SendStr` | один `send` на ответ. TCP не обязан отдать весь буфер за раз — длинный ответ (`IF`, `ZZEB`, список режимов) мог уехать обрезанным, и молча: усечение здесь не ошибка. Теперь дописываем остаток в цикле | +| `CATSerial` | порт помечался активным ДО `SerOpen`; при отказе он навсегда оставался «работающим» в `ActiveCount` и UI, а причина нигде не оседала. Открытие переехало из потока в `TCATSerialPort.Start` (синхронно), появилось свойство `LastError`, поток теперь только читает, а закрывает владелец в `Stop`. Заодно Andromeda-порт назначается только на реально поднявшийся порт | + +Формат `ZZOT` сверен отдельно: эталон читает 9 цифр как МГц с шестью знаками +после запятой (вставляет точку после третьего разряда) — численно это ровно те +же герцы, что пишем мы, поле совместимо. Мусор в `ZZOS` эталон трактует как +симплекс (`default` в `String2OffsetDirection`), и мы намеренно повторяем это, +а не отвечаем ошибкой. + +Формат ответа на `ZZ*` (`ZZ` + код + **эхо суффикса запроса** + значение) сверен +с `CATParser.ParseExtended` — совпадает; это важно для `ZZSM0;`, где запрос +несёт селектор приёмника. + --- ## 3. Что нужно сделать (актуально / есть чем закрыть) @@ -151,21 +295,30 @@ CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN. | Команда(ы) | Что | Чем закрыть | Сложность | |---|---|---|---| +| `ZZVL`, `ZZUX`/`ZZUY` | блокировка VFO (общая и раздельная) | `SetVfoLock` | низкая | +| `ZZSW`, `ZZZW` | обмен VFO A/B | `VfoSwap` | низкая | +| `ZZAD ZZAE ZZAF ZZBE ZZBF`, `ZZSG`/`ZZSH` | сдвиг VFO на nn шагов | `StepIdxToHz` + `DoTuneUp/Down`, как у `ZZAU`/`ZZBP` | низкая | | `ZZCU` | загрузка CPU (read) | телеметрия процесса | низкая | -| `ZZMY` / `ZZQR` / `ZZQS` / `ZZQM` | память каналов (запись/чтение/quick) | `ChannelStore` уже есть | средняя | -| `ZZFD` | FM-девиация (radio button) | нужен публичный `SetFMDeviation` (сейчас только поле `FFMDeviation`) | низкая | -| `ZZDM` / `ZZDA` / `ZZPO` / `ZZDY` | режим/усреднение/пик/порог спектра | UI-параметры спектра | средняя | +| `ZZSF` | фильтр «центр + ширина» одной командой | `SetFilterEdges` | низкая | +| `ZZSG`/`ZZSH`, `ZZSD` | шаг VFO B и колеса — по одному шагу | те же `SetVfoB`/`DoTune*` | низкая | +| `ZZMY ZZMZ ZZMW ZZMX ZZQR ZZQS ZZQM` | запись/чтение памяти каналов (счётчик `ZZMV` уже есть) | `ChannelStore` / `ChannelController` | средняя | +| `ZZDN`/`ZZDO`, `ZZDP`/`ZZDQ`/`ZZDR` | низ/верх водопада, сетка спектра | `FSpecRefLevel`/`FSpecRange`, per-pan дисплей | средняя | +| `ZZPD`/`ZZPE`, `ZZPY`/`ZZPZ` | пан и зум дисплея | `SetPanZoomPan` | средняя | +| `ZZSN`, `ZZZM`, `ZZZV` | серийник, модель, версия железа | `BoardUtils` | низкая | +| `ZZXN` / `ZZXV` | составные статусы RX1/VFO | сборка из готовых геттеров | средняя | +| `ZZOC`/`ZZOD`/`ZZOE` | антенны | `SetAnt936x` (порты AD936x) | средняя | +| `ZZDX` / `ZZDY` | кнопка DX и порог спотов | флаг `ShowSpots` живёт в `MainForm` — сперва поднять в контроллер | средняя | | `ZZAS` | RX2 AGC-T | — только при появлении RX2 | — | > Прежде чем реализовывать новую команду, сверяйся с форматом поля в > `Thetis/.../CAT/CATStructs.xml` (`` / ``) и телом -> метода в `CATCommands.cs`, чтобы ширина/знак значения совпадали. +> метода в `CATCommands.cs`, чтобы ширина/знак значения совпадали. И проверяй +> **смысл**: подпись у заглушки в диспетчере может врать (см. §1.2). ### 3.1 Совсем не обрабатываются (вернут `?;`) -`ZZCU` (CPU), `ZZDY` (DX threshold), `ZZPK`/`ZZPL` (речевой компрессор), -`ZZQA`/`ZZQB` (Quick Play/Rec), `ZZVF` (VAC stereo). -Из них реалистично добавить только `ZZCU` и (частично) `ZZDY`. +`ZZCU` (CPU), `ZZDY` (DX threshold), `ZZQA`/`ZZQB` (Quick Play/Rec), +`ZZVF` (VAC stereo). Из них реалистично добавить только `ZZCU` и `ZZDY`. --- @@ -176,13 +329,15 @@ CTCSS on/tone, FM repeater dir/offset, FM step, AGC-T, CTUN. | Область | Команды (примеры) | Причина | |---|---|---| -| **Второй приёмник RX2 / sub-RX** | `ZZAB ZZAS ZZBA ZZMB ZZOB`, ext `BF BG BR DO DQ DR OF OG OH OL SG SH SS` | один приёмник | -| **TX-DSP тракт** | mic gain `ZZMG`/`MG`, монитор `ZZMO`/`MO`, TX-EQ `ZZEB/ER/ET`, компрессор `ZZPK/ZZPL/CF CI CL CM`, two-tone `ZZUS`, VAC `ZZVB/ZZVF` | нет обработки передачи | -| **CW-кейер / CWX** | `ZZKM ZZKS ZZKY ZZCB ZZCD ZZCS`, `KY KS` | нет кейера | -| **RTTY / DIGI** | `ZZRA ZZRL`, ext `RF RH RM RS RY` | нет RTTY-движка | -| **RIT / XIT** | `RT XT RC RU RD` (как RIT), ext `RB RF RH` | нет смещения приёма | -| **Diversity** | `ZZDA(div) ZZDB`, ext `DA` | нет диверсити-приёма | -| **Спектр/водопад (внутр.)** | ext `SV SW SX SY SZ WT WU WV WW` | не управляется по CAT | +| **Второй приёмник RX2 / sub-RX** | `ZZAB ZZAS ZZBA ZZBB ZZBT ZZMB ZZOB ZZRS ZZSV ZZSX ZZNC ZZND ZZNF ZZNO ZZNU ZZNW ZZFJ ZZFR ZZFS ZZGU ZZLE ZZLF ZZPB ZZRY ZZXO` | один приёмник | +| **VAC / VAC2** | `ZZVC ZZVD ZZVF ZZVI ZZVK ZZVM ZZVO ZZVQ ZZVR ZZVT ZZVU ZZVV ZZVX ZZVY ZZVZ ZZYA ZZYB` | нет виртуального аудиокабеля | +| **RTTY / DIGI-офсеты** | `ZZRA ZZRB ZZRH ZZRL` | нет RTTY-движка | +| **RIT / XIT** | `RT XT RC`, `ZZRF ZZRT ZZXC ZZXD ZZXF ZZXS ZZXU` | нет тракта смещения приёма (CTUN — не замена) | +| **Diversity** | `ZZDB ZZDC ZZDD ZZDE ZZDF ZZDG ZZDH` (но не `ZZDA` — это усреднение спектра) | нет диверсити-приёма | +| **Компандер, noise gate, APF, VOX** | `ZZCP ZZCT`, `ZZGE ZZGL`, `ZZAA ZZAB ZZAT ZZAY`, `ZZVE ZZXH` | таких блоков в тракте нет (речевой компрессор — есть, это `ZZPK`/`ZZPL`) | +| **Микшер Flex 5000 / F1500, FlexWire** | `ZZWA…ZZWS`, `ZZWT…ZZWW`, `ZZFV ZZFW ZZFX ZZFY` | железо другого вендора | +| **ATU (Aries/Ganymede)** | `ZZOV ZZOW ZZOX ZZOZ ZZZA` | нет тюнера | +| **DSP-буферы, JSON-команды** | `ZZHA ZZHR ZZHT ZZHU ZZHV ZZHW ZZHX`, `ZZJP ZZJQ ZZJR ZZJS` | внутренние крутилки Thetis | ---