diff --git a/AndromedaMap.pas b/AndromedaMap.pas new file mode 100644 index 0000000..45ad88d --- /dev/null +++ b/AndromedaMap.pas @@ -0,0 +1,134 @@ +unit AndromedaMap; + +{ + AndromedaMap.pas — таблицы раскладки передней панели Apache Labs Andromeda / G2. + + Только ДАННЫЕ: перечисления действий и дефолтные таблицы + «номер кнопки → действие», «номер энкодера → параметр», + «индикатор → номер LED (ZZZI)». Без логики и без зависимостей от + контроллера — этим занимается AndromedaPanel.pas. + + Соответствие протоколу Thetis: + • действия кнопок/энкодеров — подмножество EButtonBarActions / EEncoderActions; + • номера LED-индикаторов (ZZZI nn) зеркалят дефолтную таблицу Thetis + (MakeNewAndromedaDataset): 1=MOX, 3=Tune, 7=CTUN и т.д. Свободные слоты + 12..17 заняты функциями ewsdr (Split/NB/NR/SNB/ANF/Squelch). +} + +{$mode objfpc}{$H+} + +interface + +type + // Действие кнопки панели (подмножество, поддержанное ewsdr). + TAndButton = ( + abNone, abMOX, abTune, abBandUp, abBandDown, abModeUp, abModeDown, + abMute, abNB, abNR, abSNB, abANF, abVFOSplit, abVFOSwap, abCTUN, + abCTCSS, abSquelch, abFilterUp, abFilterDown); + + // Параметр, управляемый поворотным энкодером панели. + TAndEncoder = ( + aeNone, aeAFGain, aeAGCLevel, aeDrive, aeSquelch, + aeFilterWidth, aeMulti); + + // Индикатор (LED) панели. Порядок зеркалит EIndicatorActions Thetis. + TAndIndicator = ( + aiNone, aiMOX, aiTune, aiRIT, aiXIT, aiSplit, aiCTune, aiVFOSync, + aiVFOAB, aiVFOLock, aiNB, aiNR, aiSNB, aiANF, aiSquelch, + aiCompander, aiPuresignal, aiDiversity, aiATUReady, aiShift); + +const + // Размеры дефолтных таблиц (кнопки/энкодеры нумеруются с 0 после разбора ZZZP/ZZZE). + AND_MAX_BUTTONS = 32; + AND_MAX_ENCODERS = 20; + + // Раскладка кнопок: индекс = номер кнопки (0-based), значение = действие. + // Неназначенные → abNone. Дефолт ewsdr (правится при появлении конкретной панели). + AND_BUTTON_MAP: array[0..AND_MAX_BUTTONS-1] of TAndButton = ( + abMOX, // 0 + abTune, // 1 + abBandUp, // 2 + abBandDown, // 3 + abModeUp, // 4 + abModeDown, // 5 + abNB, // 6 + abNR, // 7 + abANF, // 8 + abSNB, // 9 + abVFOSplit, // 10 + abVFOSwap, // 11 + abCTUN, // 12 + abMute, // 13 + abCTCSS, // 14 + abSquelch, // 15 + abFilterUp, // 16 + abFilterDown, // 17 + abNone, abNone, abNone, abNone, abNone, abNone, abNone, abNone, + abNone, abNone, abNone, abNone, abNone, abNone); + + // Раскладка энкодеров: индекс = номер энкодера (0-based), значение = параметр. + AND_ENCODER_MAP: array[0..AND_MAX_ENCODERS-1] of TAndEncoder = ( + aeAFGain, // 0 + aeAGCLevel, // 1 + aeDrive, // 2 + aeSquelch, // 3 + aeFilterWidth, // 4 + aeNone, aeNone, aeNone, aeNone, aeNone, + aeNone, aeNone, aeNone, aeNone, aeNone, + aeNone, aeNone, aeNone, aeNone, aeMulti); + + // Номер LED (ZZZI nn) для каждого индикатора; 0 = панель этот индикатор не имеет. + // 1..11 — дефолт Thetis; 12..17 — функции ewsdr на свободных слотах. + AND_INDICATOR_NUM: array[TAndIndicator] of Integer = ( + {aiNone} 0, + {aiMOX} 1, + {aiTune} 3, + {aiRIT} 8, + {aiXIT} 9, + {aiSplit} 12, + {aiCTune} 7, + {aiVFOSync} 0, + {aiVFOAB} 10, + {aiVFOLock} 11, + {aiNB} 13, + {aiNR} 14, + {aiSNB} 15, + {aiANF} 16, + {aiSquelch} 17, + {aiCompander} 0, + {aiPuresignal} 0, + {aiDiversity} 0, + {aiATUReady} 2, + {aiShift} 6); + +{ Возвращает действие для номера кнопки (0-based); abNone вне диапазона. } +function ButtonAction(BtnIndex: Integer): TAndButton; +{ Возвращает параметр для номера энкодера (0-based); aeNone вне диапазона. } +function EncoderParam(EncIndex: Integer): TAndEncoder; +{ Номер ZZZI для индикатора; 0 если панель его не отображает. } +function IndicatorNum(Ind: TAndIndicator): Integer; + +implementation + +function ButtonAction(BtnIndex: Integer): TAndButton; +begin + if (BtnIndex >= 0) and (BtnIndex < AND_MAX_BUTTONS) then + Result := AND_BUTTON_MAP[BtnIndex] + else + Result := abNone; +end; + +function EncoderParam(EncIndex: Integer): TAndEncoder; +begin + if (EncIndex >= 0) and (EncIndex < AND_MAX_ENCODERS) then + Result := AND_ENCODER_MAP[EncIndex] + else + Result := aeNone; +end; + +function IndicatorNum(Ind: TAndIndicator): Integer; +begin + Result := AND_INDICATOR_NUM[Ind]; +end; + +end. diff --git a/AndromedaPanel.pas b/AndromedaPanel.pas new file mode 100644 index 0000000..17fd3e4 --- /dev/null +++ b/AndromedaPanel.pas @@ -0,0 +1,221 @@ +unit AndromedaPanel; + +{ + AndromedaPanel.pas — логика передней панели Apache Labs Andromeda / G2. + + TAndromedaPanel — «мозг» панели поверх TRadioController: + • входящие события (энкодер VFO, поворот энкодера, нажатие кнопки, версия HW) + транслируются в действия контроллера по таблицам AndromedaMap; + • изменения состояния контроллера (через AddStateListener) превращаются в + сообщения подсветки ZZZI и отдаются наружу через callback SendProc. + + Архитектура: панель зависит ТОЛЬКО от TRadioController и AndromedaMap. Транспорт + (serial-порт) и связывание с CAT-движком — в CATAdapter, который создаёт панель, + даёт ей SendProc и маршрутизирует к ней callback'и движка. + + Потоки: входящие хэндлеры зовутся из потока транспорта (CAT serial) и + маршалятся в поток контроллера через FController.Invoke. Слушатель состояния + и SendProc работают из потока контроллера — формирование строки и отправка в + порт потокобезопасны (serial SendStr под своим локом). +} + +{$mode objfpc}{$H+} + +interface + +uses + SysUtils, RadioController, AndromedaMap; + +const + AND_VFO_STEP_HZ = 10; // Гц на один шаг энкодера VFO (как DoTuneUp/Down в CAT) + AND_BW_STEP_HZ = 50; // Гц на шаг энкодера ширины фильтра + MODE_COUNT_CYCLE = 8; // MODE_LSB..MODE_SAM (0..7) для перебора режимов + +type + TAndromedaSend = procedure(const S: string) of object; + + TAndromedaPanel = class + private + FController: TRadioController; + FSend: TAndromedaSend; + FConnected: Boolean; // панель прислала версию (ZZZS) + FIndState: array[0..20] of Integer; // кэш состояний LED: -1 неизв., 0/1 + // scratch для маршалинга в поток контроллера + FPendingVfoSteps: Integer; + FPendingEnc: TAndEncoder; + FPendingStep: Integer; + FPendingButton: TAndButton; + + procedure SyncVfo; + procedure SyncEncoder; + procedure SyncButton; + + procedure PushIndicator(Ind: TAndIndicator; State: Boolean); + procedure SendIndicatorNum(Num: Integer; State: Boolean); + public + constructor Create(AController: TRadioController; ASend: TAndromedaSend); + + // Вызывается при включении панели: запрос версии + (после ответа) синк LED. + procedure RequestVersion; + // Полная синхронизация всех управляемых индикаторов с текущим состоянием. + procedure SyncAllIndicators; + + // ── Входящие события панели (из потока транспорта) ── + procedure HandleVfoStep(Steps: Integer); + procedure HandleEncoder(Enc, Step: Integer); + procedure HandleButton(Btn: Integer; State, LongPress: Boolean); + procedure HandleVersion(Version: Integer); + + // ── Слушатель состояния контроллера (зарегистрировать через AddStateListener) ── + procedure OnControllerState(Sender: TObject; Field: TRadioField); + + property Connected: Boolean read FConnected; + end; + +implementation + +constructor TAndromedaPanel.Create(AController: TRadioController; ASend: TAndromedaSend); +var i: Integer; +begin + inherited Create; + FController := AController; + FSend := ASend; + FConnected := False; + for i := Low(FIndState) to High(FIndState) do FIndState[i] := -1; +end; + +procedure TAndromedaPanel.RequestVersion; +begin + if Assigned(FSend) then FSend('ZZZS;'); // панель ответит ZZZS → HandleVersion +end; + +{ ── Входящие события ───────────────────────────────────────────────────────── } + +procedure TAndromedaPanel.HandleVfoStep(Steps: Integer); +begin + if Steps = 0 then Exit; + FPendingVfoSteps := Steps; + FController.Invoke(@SyncVfo); +end; + +procedure TAndromedaPanel.SyncVfo; +begin + FController.TuneActiveBy(FPendingVfoSteps * AND_VFO_STEP_HZ); +end; + +procedure TAndromedaPanel.HandleEncoder(Enc, Step: Integer); +var p: TAndEncoder; +begin + p := EncoderParam(Enc); + if (p = aeNone) or (Step = 0) then Exit; + FPendingEnc := p; + FPendingStep := Step; + FController.Invoke(@SyncEncoder); +end; + +procedure TAndromedaPanel.SyncEncoder; +begin + case FPendingEnc of + aeAFGain: FController.SetVolume(FController.FVolume + FPendingStep); + aeAGCLevel: FController.SetAGCTop(FController.FAGCTop + FPendingStep); + aeDrive: FController.SetDrive(FController.FDrivePercent + FPendingStep); + aeSquelch: FController.SetFMSquelchLevel(FController.FFMSQLevel + FPendingStep); + aeFilterWidth: FController.SetFilterBW(FController.FFilterBW + FPendingStep * AND_BW_STEP_HZ); + // aeMulti — мультифункц. энкодер: пока не назначен + end; +end; + +procedure TAndromedaPanel.HandleButton(Btn: Integer; State, LongPress: Boolean); +var a: TAndButton; +begin + if not State then Exit; // реагируем на нажатие; отпускание/long-press — позже + a := ButtonAction(Btn); + if a = abNone then Exit; + FPendingButton := a; + FController.Invoke(@SyncButton); +end; + +procedure TAndromedaPanel.SyncButton; +begin + case FPendingButton of + abMOX: FController.SetMOX(not FController.FTransmitting); + abTune: FController.SetTune(not FController.FTuning); + abBandUp: FController.BandUp; + abBandDown: FController.BandDown; + abModeUp: FController.SetMode((FController.FMode + 1) mod MODE_COUNT_CYCLE); + abModeDown: FController.SetMode((FController.FMode + MODE_COUNT_CYCLE - 1) mod MODE_COUNT_CYCLE); + abMute: FController.SetMute(not FController.FMuted); + abNB: if FController.FNBMode > 0 then FController.SetNB(0) else FController.SetNB(1); + abNR: if FController.FNRMode > 0 then FController.SetNR(0) else FController.SetNR(1); + abSNB: FController.SetSNB(not FController.FSNB); + abANF: FController.SetANF(not FController.FANF); + abVFOSplit: FController.SetSplit(not FController.FSplitTxB); + abVFOSwap: FController.SetActiveVfo(1 - FController.FActiveVfo); + abCTUN: FController.SetCTun(not FController.FCTun); + abCTCSS: FController.SetFMCTCSS(not FController.FFMCTCSSOn); + abSquelch: FController.SetFMSquelch(not FController.FFMSQOn); + abFilterUp: FController.SetFilterIdx(FController.FFilter + 1); + abFilterDown:FController.SetFilterIdx(FController.FFilter - 1); + end; +end; + +procedure TAndromedaPanel.HandleVersion(Version: Integer); +begin + FConnected := True; + SyncAllIndicators; // панель подключилась — выставить актуальную подсветку +end; + +{ ── Индикаторы (подсветка) ─────────────────────────────────────────────────── } + +procedure TAndromedaPanel.SendIndicatorNum(Num: Integer; State: Boolean); +var v: Integer; +begin + if (Num <= 0) or (Num > High(FIndState)) then Exit; + if State then v := 1 else v := 0; + if FIndState[Num] = v then Exit; // не слать повтор + FIndState[Num] := v; + if Assigned(FSend) then + FSend('ZZZI' + Format('%.2d', [Num]) + IntToStr(v) + ';'); +end; + +procedure TAndromedaPanel.PushIndicator(Ind: TAndIndicator; State: Boolean); +begin + if not FConnected then Exit; + SendIndicatorNum(IndicatorNum(Ind), State); +end; + +procedure TAndromedaPanel.SyncAllIndicators; +begin + // FConnected уже True к моменту вызова из HandleVersion + PushIndicator(aiMOX, FController.FTransmitting); + PushIndicator(aiTune, FController.FTuning); + PushIndicator(aiCTune, FController.FCTun); + PushIndicator(aiVFOAB, FController.FActiveVfo = 1); + PushIndicator(aiSplit, FController.FSplitTxB); + PushIndicator(aiNB, FController.FNBMode > 0); + PushIndicator(aiNR, FController.FNRMode > 0); + PushIndicator(aiSNB, FController.FSNB); + PushIndicator(aiANF, FController.FANF); + PushIndicator(aiSquelch, FController.FFMSQOn); +end; + +procedure TAndromedaPanel.OnControllerState(Sender: TObject; Field: TRadioField); +begin + if not FConnected then Exit; + case Field of + rfTransmitting: PushIndicator(aiMOX, FController.FTransmitting); + rfTuning: PushIndicator(aiTune, FController.FTuning); + rfCTun: PushIndicator(aiCTune, FController.FCTun); + rfActiveVfo: begin + PushIndicator(aiVFOAB, FController.FActiveVfo = 1); + PushIndicator(aiSplit, FController.FSplitTxB); + end; + rfNB: PushIndicator(aiNB, FController.FNBMode > 0); + rfNR: PushIndicator(aiNR, FController.FNRMode > 0); + rfSNB: PushIndicator(aiSNB, FController.FSNB); + rfANF: PushIndicator(aiANF, FController.FANF); + rfFMSQ: PushIndicator(aiSquelch, FController.FFMSQOn); + end; +end; + +end. diff --git a/CATAdapter.pas b/CATAdapter.pas index fe7ac9e..f19c123 100644 --- a/CATAdapter.pas +++ b/CATAdapter.pas @@ -20,7 +20,8 @@ interface uses Classes, SysUtils, - RadioController, CATEngine, CATSerial, CATTcp, Settings, WDSPEngine; + RadioController, CATEngine, CATSerial, CATTcp, Settings, WDSPEngine, + AndromedaPanel; type TCATAdapter = class @@ -29,6 +30,7 @@ type FEngine: TCATEngine; FSerial: TCATSerialManager; FTcp: TCATTcpServer; + FPanel: TAndromedaPanel; // передняя панель Andromeda/G2 (если включена) // Параметры команды для маршалинга в поток контроллера (через Invoke). FSyncFreq: Double; FSyncInt: Integer; @@ -93,6 +95,12 @@ type procedure SetAGCTop(V: Integer); procedure SetCTun(V: Boolean); + // ── Andromeda: форвард callback'ов движка в панель (из потока транспорта) ── + procedure PanelVfoStep(V: Integer); + procedure PanelEncoder(Enc, Step: Integer); + procedure PanelButton(Btn: Integer; State, LongPress: Boolean); + procedure PanelVersion(V: Integer); + // ── Sync-методы (выполняются в потоке контроллера) ── procedure SyncVfoA; procedure SyncVfoB; @@ -194,10 +202,18 @@ begin Ctx.SetAGCTop := @SetAGCTop; Ctx.GetCTun := @GetCTun; Ctx.SetCTun := @SetCTun; + Ctx.OnPanelVfoStep := @PanelVfoStep; + Ctx.OnPanelEncoder := @PanelEncoder; + Ctx.OnPanelButton := @PanelButton; + Ctx.OnPanelVersion := @PanelVersion; FEngine := TCATEngine.Create(Ctx); FSerial := TCATSerialManager.Create(FEngine); FTcp := TCATTcpServer.Create(FEngine); + + // Передняя панель: отправляет наружу через Andromeda-порт, слушает состояние. + FPanel := TAndromedaPanel.Create(FController, @FSerial.SendToAndromeda); + FController.AddStateListener(@FPanel.OnControllerState); end; destructor TCATAdapter.Destroy; @@ -205,6 +221,7 @@ begin if Assigned(FTcp) then begin FTcp.Stop; FreeAndNil(FTcp); end; if Assigned(FSerial) then begin FSerial.StopAll; FreeAndNil(FSerial); end; FreeAndNil(FEngine); + FreeAndNil(FPanel); inherited Destroy; end; @@ -225,8 +242,11 @@ begin 2: Cfgs[i].Parity := cspEven; else Cfgs[i].Parity := cspNone; end; + Cfgs[i].Andromeda := G.CATSerialAndromeda[i]; end; FSerial.ApplyConfig(Cfgs); + // Если назначен Andromeda-порт — запросить версию панели (рукопожатие ZZZS). + if FSerial.HasAndromeda and Assigned(FPanel) then FPanel.RequestVersion; FTcp.Stop; if G.CATTcpEnabled then begin @@ -442,4 +462,17 @@ begin FSyncBool := V; FController.Invoke(@SyncCTun); end; procedure TCATAdapter.SyncCTun; begin FController.SetCTun(FSyncBool); end; +// ── Andromeda: форвард в панель (сама маршалит действия в поток контроллера) ── +procedure TCATAdapter.PanelVfoStep(V: Integer); +begin if Assigned(FPanel) then FPanel.HandleVfoStep(V); end; + +procedure TCATAdapter.PanelEncoder(Enc, Step: Integer); +begin if Assigned(FPanel) then FPanel.HandleEncoder(Enc, Step); end; + +procedure TCATAdapter.PanelButton(Btn: Integer; State, LongPress: Boolean); +begin if Assigned(FPanel) then FPanel.HandleButton(Btn, State, LongPress); end; + +procedure TCATAdapter.PanelVersion(V: Integer); +begin if Assigned(FPanel) then FPanel.HandleVersion(V); end; + end. diff --git a/CATEngine.pas b/CATEngine.pas index 169e6d4..c9c98fb 100644 --- a/CATEngine.pas +++ b/CATEngine.pas @@ -48,6 +48,9 @@ type TCATSetIntProc = procedure(V: Integer) of object; TCATSetBoolProc = procedure(V: Boolean) of object; TCATProc = procedure of object; + // Andromeda front panel input + TCATPanelEncoderProc = procedure(Enc, Step: Integer) of object; + TCATPanelButtonProc = procedure(Btn: Integer; State, LongPress: Boolean) of object; TCATContext = record GetVfoA: TCATGetDoubleFunc; @@ -106,6 +109,11 @@ type SetAGCTop: TCATSetIntProc; // AGC-T порог, dB GetCTun: TCATGetBoolFunc; // Click-tune (CTUN) вкл/выкл SetCTun: TCATSetBoolProc; + // Andromeda front panel input (ZZZD/ZZZU/ZZZE/ZZZP/ZZZS) + OnPanelVfoStep: TCATSetIntProc; // знаковое число шагов энкодера VFO + OnPanelEncoder: TCATPanelEncoderProc; // номер энкодера (0-based) + знаковый шаг + OnPanelButton: TCATPanelButtonProc; // номер кнопки (0-based) + state/long-press + OnPanelVersion: TCATSetIntProc; // версия HW/SW панели (рукопожатие) end; TCATEngine = class @@ -321,6 +329,8 @@ type function ZZYR(const s: string): string; function ZZZA(const s: string): string; function ZZZB: string; + function ZZZD(const s: string): string; + function ZZZE(const s: string): string; function ZZZM(const s: string): string; function ZZZP(const s: string): string; function ZZZQ(const s: string): string; @@ -1348,8 +1358,8 @@ begin else if ext = 'YR' then ans := ZZYR(s) else if ext = 'ZA' then ans := ZZZA(s) else if ext = 'ZB' then begin ZZZB; ans := ''; end - else if ext = 'ZD' then ans := '' // STUB: system D - else if ext = 'ZE' then ans := '' // STUB: system E + else if ext = 'ZD' then ans := ZZZD(s) + else if ext = 'ZE' then ans := ZZZE(s) else if ext = 'ZM' then ans := ZZZM(s) else if ext = 'ZN' then ans := '' // STUB: system N else if ext = 'ZO' then ans := ZZZO(s) @@ -2242,20 +2252,78 @@ begin if Length(s) > 0 then Result := '' else Result := '0'; end; function TCATEngine.ZZZA(const s: string): string; begin if Length(s) > 0 then Result := '' else Result := '0'; end; function TCATEngine.ZZZB: string; begin Result := ''; end; + +function TCATEngine.ZZZD(const s: string): string; +// ZZZD: Andromeda — энкодер VFO повёрнут вниз на nn шагов (write-only). +begin + if Length(s) > 0 then begin + if Assigned(FCtx.OnPanelVfoStep) then FCtx.OnPanelVfoStep(-StrToIntDef(s, 0)); + Result := ''; + end else + Result := CAT_ERROR; +end; + +function TCATEngine.ZZZE(const s: string): string; +// ZZZE: Andromeda — поворот энкодера. Кодировка Thetis: верхние 2 цифры — номер +// энкодера (01..20 = +шаг, 51..70 = −шаг), нижняя цифра — величина шага. +var raw, enc, step: Integer; +begin + if Length(s) > 0 then begin + raw := StrToIntDef(s, 0); + step := raw mod 10; + enc := raw div 10; + if (enc >= 1) and (enc <= 20) then begin + if Assigned(FCtx.OnPanelEncoder) then FCtx.OnPanelEncoder(enc - 1, step); + end else if (enc >= 51) and (enc <= 70) then begin + if Assigned(FCtx.OnPanelEncoder) then FCtx.OnPanelEncoder(enc - 51, -step); + end; + Result := ''; + end else + Result := CAT_ERROR; +end; + function TCATEngine.ZZZM(const s: string): string; begin if Length(s) > 0 then Result := '' else Result := '0'; end; + function TCATEngine.ZZZP(const s: string): string; -begin if Length(s) > 0 then Result := '' else Result := '0'; end; +// ZZZP: Andromeda — нажатие кнопки (write-only). Кодировка: nn*10 + state, +// state: 0=отпущена, 1=нажата, 2=long-press. nn — номер кнопки (1-based). +var raw, btn: Integer; state, long: Boolean; +begin + if Length(s) > 0 then begin + raw := StrToIntDef(s, 0); + state := (raw mod 10) = 1; + long := (raw mod 10) = 2; + btn := raw div 10; + if Assigned(FCtx.OnPanelButton) then FCtx.OnPanelButton(btn - 1, state, long); + Result := ''; + end else + Result := CAT_ERROR; +end; function TCATEngine.ZZZQ(const s: string): string; begin if Length(s) > 0 then Result := '' else Result := '0'; end; function TCATEngine.ZZZR(const s: string): string; begin if Length(s) > 0 then Result := '' else Result := '0'; end; function TCATEngine.ZZZS(const s: string): string; -begin if Length(s) > 0 then Result := '' else Result := '0'; end; +// ZZZS: Andromeda — панель сообщает версию HW/SW (write-only, рукопожатие). +begin + if Length(s) > 0 then begin + if Assigned(FCtx.OnPanelVersion) then FCtx.OnPanelVersion(StrToIntDef(s, 0)); + Result := ''; + end else + Result := CAT_ERROR; +end; function TCATEngine.ZZZT(const s: string): string; begin if Length(s) > 0 then Result := '' else Result := '0'; end; function TCATEngine.ZZZU(const s: string): string; -begin if Length(s) > 0 then Result := '' else Result := '0'; end; +// ZZZU: Andromeda — энкодер VFO повёрнут вверх на nn шагов (write-only). +begin + if Length(s) > 0 then begin + if Assigned(FCtx.OnPanelVfoStep) then FCtx.OnPanelVfoStep(StrToIntDef(s, 0)); + Result := ''; + end else + Result := CAT_ERROR; +end; function TCATEngine.ZZZV(const s: string): string; begin if Length(s) > 0 then Result := '' else Result := '0'; end; function TCATEngine.ZZZW(const s: string): string; diff --git a/CATSerial.pas b/CATSerial.pas index 33f165b..3eca08a 100644 --- a/CATSerial.pas +++ b/CATSerial.pas @@ -47,6 +47,7 @@ type DataBits: Integer; // 7 or 8 StopBits: Integer; // 1 or 2 Parity: TCATSerialParity; + Andromeda: Boolean; // порт — передняя панель Andromeda/G2 (push ZZZI наружу) end; {$IFDEF CAT_SERIAL_STUB} @@ -95,12 +96,16 @@ type private FPorts: array[0..CAT_SERIAL_MAX_PORTS-1] of TCATSerialPort; FEngine: TCATEngine; + FAndromedaPort: TCATSerialPort; // первый активный порт с Andromeda=true (или nil) public constructor Create(AEngine: TCATEngine); destructor Destroy; override; procedure ApplyConfig(const Configs: array of TCATSerialConfig); procedure StopAll; function ActiveCount: Integer; + // Передняя панель: есть ли активный Andromeda-порт и отправка строки в него. + function HasAndromeda: Boolean; + procedure SendToAndromeda(const S: string); // сигнатура совместима с TAndromedaSend end; implementation @@ -267,6 +272,7 @@ var i: Integer; begin inherited Create; FEngine := AEngine; + FAndromedaPort := nil; for i := 0 to CAT_SERIAL_MAX_PORTS-1 do FPorts[i] := nil; end; @@ -288,12 +294,25 @@ 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 + FAndromedaPort := FPorts[i]; end; end; +function TCATSerialManager.HasAndromeda: Boolean; +begin + Result := Assigned(FAndromedaPort); +end; + +procedure TCATSerialManager.SendToAndromeda(const S: string); +begin + if Assigned(FAndromedaPort) then FAndromedaPort.SendStr(S); +end; + procedure TCATSerialManager.StopAll; var i: Integer; begin + FAndromedaPort := nil; // указывает в FPorts[] — обнулить ДО освобождения for i := 0 to CAT_SERIAL_MAX_PORTS-1 do begin if Assigned(FPorts[i]) then begin FPorts[i].Stop; diff --git a/MainForm.pas b/MainForm.pas index 43dc0e4..a6bd290 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -476,6 +476,7 @@ type const SerEnabled: array of Boolean; const SerPort: array of string; const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; + const SerAndromeda: array of Boolean; TcpEnabled: Boolean; TcpPort: Integer); procedure OnAlexSettingsChange(const A: TAlexSettings); procedure OnXvtrSettingsChange(const X: TXvtrSettings); @@ -5872,6 +5873,7 @@ begin FCATLastGlobal.CATSerialDataBits, FCATLastGlobal.CATSerialStopBits, FCATLastGlobal.CATSerialParity, + FCATLastGlobal.CATSerialAndromeda, FCATLastGlobal.CATTcpEnabled, FCATLastGlobal.CATTcpPort); // Перечисляем PA устройства @@ -5940,6 +5942,7 @@ procedure TMainForm.OnCATSettingsChange( const SerEnabled: array of Boolean; const SerPort: array of string; const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; + const SerAndromeda: array of Boolean; TcpEnabled: Boolean; TcpPort: Integer); var i: Integer; begin @@ -5951,6 +5954,7 @@ begin if i <= High(SerDataBits) then FCATLastGlobal.CATSerialDataBits[i] := SerDataBits[i]; if i <= High(SerStopBits) then FCATLastGlobal.CATSerialStopBits[i] := SerStopBits[i]; if i <= High(SerParity) then FCATLastGlobal.CATSerialParity[i] := SerParity[i]; + if i <= High(SerAndromeda)then FCATLastGlobal.CATSerialAndromeda[i]:= SerAndromeda[i]; end; FCATLastGlobal.CATTcpEnabled := TcpEnabled; FCATLastGlobal.CATTcpPort := TcpPort; @@ -5962,6 +5966,7 @@ begin FController.FLoadedGlobal.CATSerialDataBits := FCATLastGlobal.CATSerialDataBits; FController.FLoadedGlobal.CATSerialStopBits := FCATLastGlobal.CATSerialStopBits; FController.FLoadedGlobal.CATSerialParity := FCATLastGlobal.CATSerialParity; + FController.FLoadedGlobal.CATSerialAndromeda := FCATLastGlobal.CATSerialAndromeda; FController.FLoadedGlobal.CATTcpEnabled := FCATLastGlobal.CATTcpEnabled; FController.FLoadedGlobal.CATTcpPort := FCATLastGlobal.CATTcpPort; FCATAdapter.ApplySettings(FCATLastGlobal); diff --git a/RadioController.pas b/RadioController.pas index b9dee66..094993e 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -127,6 +127,7 @@ type private FOnInvoke: TUIInvokeEvent; FOnStateChanged: TRadioStateEvent; + FStateListeners: array of TRadioStateEvent; // доп. наблюдатели (CAT/Andromeda), помимо FOnStateChanged (MainForm) FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA FOnAudioConsume: TAudioConsumeEvent; // web-перехват RX-аудио (если активен клиент) FLocalAudio: Boolean; // локальный звук (динамик/мик ПК): GUI=True, демон=False @@ -577,6 +578,9 @@ type // ---- События наружу ---- property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke; property OnStateChanged: TRadioStateEvent read FOnStateChanged write FOnStateChanged; + // Доп. наблюдатели состояния (помимо единственного OnStateChanged, который + // занимает MainForm). Нужны CAT/Andromeda для push индикаторов панели. + procedure AddStateListener(L: TRadioStateEvent); // Пост-тюн хук: вызывается в КОНЦЕ SetVfoA (после Changed(rfVfoA), т.е. вне // рендера) — сюда вешается канальная оркестрация (ChannelController.OnVfoTuned), // чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless. @@ -676,8 +680,18 @@ begin end; procedure TRadioController.Changed(Field: TRadioField); +var i: Integer; begin if Assigned(FOnStateChanged) then FOnStateChanged(Self, Field); + for i := 0 to High(FStateListeners) do + if Assigned(FStateListeners[i]) then FStateListeners[i](Self, Field); +end; + +procedure TRadioController.AddStateListener(L: TRadioStateEvent); +begin + if not Assigned(L) then Exit; + SetLength(FStateListeners, Length(FStateListeners) + 1); + FStateListeners[High(FStateListeners)] := L; end; procedure TRadioController.PushNetworkState; diff --git a/Settings.pas b/Settings.pas index afdd832..4d2c209 100644 --- a/Settings.pas +++ b/Settings.pas @@ -284,6 +284,7 @@ type CATSerialDataBits: array[0..3] of Integer; // 7 или 8 CATSerialStopBits: array[0..3] of Integer; // 1 или 2 CATSerialParity: array[0..3] of Integer; // 0=None, 1=Odd, 2=Even + CATSerialAndromeda:array[0..3] of Boolean; // порт — передняя панель Andromeda/G2 // TCP CAT сервер (один, многоклиентский) CATTcpEnabled: Boolean; CATTcpPort: Integer; // порт, по умолчанию 19090 @@ -464,6 +465,7 @@ begin G.CATSerialDataBits[i] := 8; G.CATSerialStopBits[i] := 1; G.CATSerialParity[i] := 0; // None + G.CATSerialAndromeda[i]:= False; end; G.CATTcpEnabled := False; G.CATTcpPort := 19090; @@ -696,6 +698,7 @@ begin G.CATSerialDataBits[i] := JI(GObj,'cat_serial_dbits_'+IntToStr(i), 8); G.CATSerialStopBits[i] := JI(GObj,'cat_serial_sbits_'+IntToStr(i), 1); G.CATSerialParity[i] := JI(GObj,'cat_serial_parity_'+IntToStr(i), 0); + G.CATSerialAndromeda[i]:= JB(GObj,'cat_serial_androm_'+IntToStr(i), False); end; G.CATTcpEnabled := JB(GObj,'cat_tcp_enabled', False); G.CATTcpPort := JI(GObj,'cat_tcp_port', 19090); @@ -796,6 +799,7 @@ begin JW(O,'cat_serial_dbits_'+IntToStr(i),G.CATSerialDataBits[i]); JW(O,'cat_serial_sbits_'+IntToStr(i),G.CATSerialStopBits[i]); JW(O,'cat_serial_parity_'+IntToStr(i),G.CATSerialParity[i]); + JW(O,'cat_serial_androm_'+IntToStr(i),G.CATSerialAndromeda[i]); end; JW(O,'cat_tcp_enabled',G.CATTcpEnabled); JW(O,'cat_tcp_port',G.CATTcpPort); @@ -932,6 +936,7 @@ begin JW(O, 'ser_dbits_' +IntToStr(i), G.CATSerialDataBits[i]); JW(O, 'ser_sbits_' +IntToStr(i), G.CATSerialStopBits[i]); JW(O, 'ser_parity_'+IntToStr(i), G.CATSerialParity[i]); + JW(O, 'ser_androm_'+IntToStr(i), G.CATSerialAndromeda[i]); end; end; @@ -954,6 +959,7 @@ begin G.CATSerialDataBits[i] := JI(O, 'ser_dbits_' +IntToStr(i), 8); G.CATSerialStopBits[i] := JI(O, 'ser_sbits_' +IntToStr(i), 1); G.CATSerialParity[i] := JI(O, 'ser_parity_'+IntToStr(i), 0); + G.CATSerialAndromeda[i]:= JB(O, 'ser_androm_'+IntToStr(i), False); end; end; diff --git a/SettingsForm.pas b/SettingsForm.pas index c2fa71c..c3f82fa 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -67,6 +67,7 @@ type const SerDataBits: array of Integer; const SerStopBits: array of Integer; const SerParity: array of Integer; + const SerAndromeda: array of Boolean; TcpEnabled: Boolean; TcpPort: Integer) of object; TOnAlexChange = procedure(const A: TAlexSettings) of object; TOnXvtrChange = procedure(const X: TXvtrSettings) of object; @@ -263,6 +264,7 @@ type FCATSerialDataBits: array[0..3] of TFlatComboBox; FCATSerialStopBits: array[0..3] of TFlatComboBox; FCATSerialParity: array[0..3] of TFlatComboBox; + FCATSerialAndromeda:array[0..3] of TFlatCheckBox; FCATTcpEn: TFlatCheckBox; FCATTcpPort: TFlatSpinEdit; @@ -415,6 +417,7 @@ type const SerEnabled: array of Boolean; const SerPort: array of string; const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; + const SerAndromeda: array of Boolean; TcpEnabled: Boolean; TcpPort: Integer); procedure LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0); procedure LoadAlexSettings(const A: TAlexSettings; BoardType: Integer); @@ -2621,7 +2624,7 @@ const LW = 120; CX = PAD + LW + 8; GRP_W = 320; - GRP_H = 240; + GRP_H = 272; GRP_GAP = 16; CMB_W = 140; EDT_W = 160; @@ -2712,6 +2715,16 @@ begin Cmb.Items.Add('None'); Cmb.Items.Add('Odd'); Cmb.Items.Add('Even'); Cmb.ItemIndex := 0; // None FCATSerialParity[i] := Cmb; + + Chk := TFlatCheckBox.Create(Self); + Chk.Parent := Grp; + Chk.Caption := 'Andromeda / G2 front panel'; + Chk.SetBounds(PAD, R1 + 6 * STEP, GRP_W - PAD * 2, 22); + Chk.Font.Color := CLR_TEXT; + Chk.Font.Name := UI_FONT; Chk.Font.Size := 9; + Chk.Tag := i; + Chk.OnChange := OnCATSerialChange; + FCATSerialAndromeda[i] := Chk; end; // TCP server group below the two rows of serial port panels @@ -2747,6 +2760,7 @@ var SerEn: array[0..3] of Boolean; SerPort: array[0..3] of string; SerBaud, SerData, SerStop, SerPar: array[0..3] of Integer; + SerAndr: array[0..3] of Boolean; i, Idx: Integer; begin if FLoading then Exit; @@ -2762,9 +2776,10 @@ begin SerBaud[i] := 9600; if FCATSerialDataBits[i].ItemIndex = 0 then SerData[i] := 7 else SerData[i] := 8; if FCATSerialStopBits[i].ItemIndex = 1 then SerStop[i] := 2 else SerStop[i] := 1; - SerPar[i] := FCATSerialParity[i].ItemIndex; + SerPar[i] := FCATSerialParity[i].ItemIndex; + SerAndr[i] := FCATSerialAndromeda[i].Checked; end; - FOnCATChange(SerEn, SerPort, SerBaud, SerData, SerStop, SerPar, + FOnCATChange(SerEn, SerPort, SerBaud, SerData, SerStop, SerPar, SerAndr, FCATTcpEn.Checked, FCATTcpPort.Value); end; @@ -2782,6 +2797,7 @@ procedure TSettingsForm.LoadCATSettings( const SerEnabled: array of Boolean; const SerPort: array of string; const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; + const SerAndromeda: array of Boolean; TcpEnabled: Boolean; TcpPort: Integer); var i, j, Idx: Integer; @@ -2811,6 +2827,8 @@ begin end; if i <= High(SerParity) then FCATSerialParity[i].ItemIndex := EnsureRange(SerParity[i], 0, 2); + if i <= High(SerAndromeda) then + FCATSerialAndromeda[i].Checked := SerAndromeda[i]; end; FCATTcpEn.Checked := TcpEnabled; FCATTcpPort.Value := EnsureRange(TcpPort, 1, 65535); diff --git a/doc/ANDROMEDA.md b/doc/ANDROMEDA.md new file mode 100644 index 0000000..f18fa0d --- /dev/null +++ b/doc/ANDROMEDA.md @@ -0,0 +1,177 @@ +# Поддержка передней панели Andromeda / G2 в EWSDR + +Ветка разработки: `feature/cat-commands-expansion`. +Дата последнего обновления: 2026-06-26. + +Документ описывает поддержку аппаратной передней панели Apache Labs +**Andromeda / ANAN-G2** (поворотные энкодеры, кнопки, LED-индикаторы, LCD). +За эталон протокола взят Thetis (`Andromeda/Andromeda.cs`, `CAT/CATCommands.cs`). + +См. также [CAT_STATUS.md](CAT_STATUS.md) — общий статус CAT-подсистемы. + +--- + +## 1. Что такое Andromeda и чем отличается от обычного CAT + +Andromeda — это **не отдельный набор команд**, а двунаправленный протокол панели +поверх CAT-фрейминга. В отличие от обычного CAT (реактивный «запрос → ответ»), +панель требует, чтобы хост **сам инициативно отправлял** обновления подсветки. + +``` + ZZZD/ZZZU/ZZZE/ZZZP/ZZZS (панель → хост: ввод) + ┌─────────┐ ───────────────────────────► ┌──────────┐ + │ Панель │ │ EWSDR │ + │ G2 / │ ◄─────────────────────────── │ (хост) │ + │Andromeda│ ZZZI / ZZMF / ZZZS; └──────────┘ + └─────────┘ (хост → панель: подсветка, LCD, рукопожатие) +``` + +| Направление | Команды | Назначение | +|---|---|---| +| **Панель → хост** | `ZZZS` | панель сообщает версию HW/SW (рукопожатие) | +| | `ZZZU` / `ZZZD` | энкодер VFO повёрнут вверх/вниз на nn шагов | +| | `ZZZE` | энкодер #ee повёрнут (01–20 = +шаг, 51–70 = −шаг) | +| | `ZZZP` | кнопка #bb: s=1 нажата, 2 long-press, 0 отпущена | +| **Хост → панель** | `ZZZI` | LED-индикатор #nn (00–19) вкл (1)/выкл (0) | +| | `ZZMF<30 цифр>` | текст мультифункц. энкодера на LCD (пока **не реализован**) | +| | `ZZZS;` | запрос версии у панели | + +--- + +## 2. Архитектура реализации + +Логика панели вынесена в отдельные юниты; в существующих файлах — только тонкие +хуки. Зависимости однонаправленные, без циклов. + +``` + ┌────────────────────┐ + serial-порт ──────► │ TCATEngine │ разбор ZZZ* → generic callbacks + (Andromeda) │ (CATEngine.pas) │ (OnPanelVfoStep/Encoder/Button/Version) + └─────────┬──────────┘ + │ callbacks + ┌─────────▼──────────┐ создаёт/связывает + │ TCATAdapter │ ◄───────────────────┐ + │ (CATAdapter.pas) │ │ + └─────────┬──────────┘ │ + SendProc (ZZZI/ZZZS;) │ ▲ AddStateListener │ + на Andromeda-порт ▼ │ (push индикаторов) │ + ┌────────────────────┐ ┌────────────┴───────┐ + │ TAndromedaPanel │ ───────►│ TRadioController │ + │ (AndromedaPanel.pas)│ Invoke │ (ядро) │ + │ использует ▼ │ действия└────────────────────┘ + │ AndromedaMap.pas │ (таблицы раскладки — только данные) + └────────────────────┘ +``` + +| Файл | Роль | +|---|---| +| **`AndromedaMap.pas`** | Только данные: enum'ы `TAndButton` / `TAndEncoder` / `TAndIndicator`, дефолтные таблицы раскладки кнопок/энкодеров и номера LED (`ZZZI`). Без логики и зависимостей. | +| **`AndromedaPanel.pas`** | `TAndromedaPanel` — «мозг»: входящие события → действия контроллера (свой маршалинг через `FController.Invoke`); подписка на состояние контроллера → построение `ZZZI` → отправка через callback `SendProc`. Зависит только от `RadioController` + `AndromedaMap`. | +| `CATEngine.pas` | Разбор входящих `ZZZD/ZZZU/ZZZE/ZZZP/ZZZS` в 4 generic-callback'а `TCATContext`. Движок остаётся чистым парсером, про Andromeda «не знает». | +| `CATAdapter.pas` | Тонкий клей: создаёт панель, маршрутизирует callback'и движка в неё, даёт `SendProc` на Andromeda-порт, регистрирует панель как слушателя состояния, инициирует рукопожатие. | +| `CATSerial.pas` | Флаг `Andromeda` в конфиге порта + `HasAndromeda` / `SendToAndromeda`. | +| `RadioController.pas` | `AddStateListener` — multicast-наблюдатель состояния (push не «ворует» единственный `OnStateChanged`, занятый MainForm). | +| `Settings.pas` / `SettingsForm.pas` / `MainForm.pas` | Поле `CATSerialAndromeda[0..3]`, персист, чекбокс на каждом serial-порту. | + +### Потоки +- Входящие хэндлеры (`HandleVfoStep/Encoder/Button/Version`) зовутся из потока + CAT-транспорта и маршалят действия в поток контроллера через `FController.Invoke` + (GUI = `TThread.Synchronize`). +- Слушатель состояния и `SendProc` работают из потока, вызвавшего `Changed()`; + отправка в порт потокобезопасна (serial `SendStr` под своим локом). + +--- + +## 3. Раскладка по умолчанию (AndromedaMap.pas) + +Раскладка **зашита в коде** (первая версия). Правится в `AndromedaMap.pas`. + +### Кнопки (`ZZZP`, индекс 0-based после разбора) +| # | Действие | # | Действие | +|---|---|---|---| +| 0 | MOX | 9 | SNB | +| 1 | Tune (TUN) | 10 | Split (TX VFO-B) | +| 2 | Band Up | 11 | VFO Swap (актив. A/B) | +| 3 | Band Down | 12 | CTUN | +| 4 | Mode Up | 13 | Mute | +| 5 | Mode Down | 14 | CTCSS | +| 6 | NB | 15 | Squelch on/off | +| 7 | NR | 16 | Filter Up | +| 8 | ANF | 17 | Filter Down | + +### Энкодеры (`ZZZE`, индекс 0-based) +| # | Параметр | Шаг | +|---|---|---| +| 0 | AF Gain (громкость) | ±1 | +| 1 | AGC-T | ±1 dB | +| 2 | Drive (мощность) | ±1 % | +| 3 | Squelch level | ±1 | +| 4 | Filter width | ±50 Гц (`AND_BW_STEP_HZ`) | +| VFO | энкодер VFO (`ZZZU/ZZZD`) | ±10 Гц/шаг (`AND_VFO_STEP_HZ`) | + +### Индикаторы (`ZZZI`, номера зеркалят дефолт Thetis) +| LED # | Индикатор | LED # | Индикатор | +|---|---|---|---| +| 1 | MOX | 12 | Split | +| 2 | ATU Ready *(нет в ewsdr)* | 13 | NB | +| 3 | Tune | 14 | NR | +| 6 | Shift *(нет)* | 15 | SNB | +| 7 | CTUN | 16 | ANF | +| 8/9 | RIT/XIT *(нет)* | 17 | Squelch | +| 10 | VFO A/B | | | +| 11 | VFO Lock *(нет)* | | | + +Драйвятся реально: **MOX, Tune, CTUN, VFO A/B, Split, NB, NR, SNB, ANF, Squelch**. +Остальные слоты дефолтной таблицы Thetis (ATU/Shift/RIT/XIT/VFOLock) в ewsdr +функций не имеют и не зажигаются. + +--- + +## 4. Как включить + +1. Подключить панель как обычный serial CAT-порт (USB). +2. Settings → CAT → выбрать нужный Serial Port, поставить **Enable** и параметры + порта (скорость и т.д. — как требует панель). +3. Поставить галку **«Andromeda / G2 front panel»** на этом порту. +4. При применении настроек EWSDR пошлёт `ZZZS;`; панель ответит версией — после + этого включается push индикаторов и панель управляет радио. + +Только **один** порт может быть Andromeda-портом (первый помеченный). + +Настройки сохраняются ключами `cat_serial_androm_N` / `ser_androm_N` +(N = 0..3) в JSON-конфиге. + +--- + +## 5. Что сделано / что осталось + +### Сделано (первая версия) +- Транспорт: **serial** (USB CAT). +- Вход: `ZZZD/ZZZU` (VFO), `ZZZE` (энкодеры), `ZZZP` (кнопки), `ZZZS` (рукопожатие). +- Выход: **`ZZZI`** — LED-индикаторы (push от `OnStateChanged`, дедуп повторов). +- Раскладка — зашитый дефолт. +- UI-чекбокс на каждый serial-порт + персист. + +### Осталось / возможные улучшения +| Задача | Заметки | +|---|---| +| **`ZZMF`** — текст на LCD мультифункц. энкодера | кодировка: 15 ASCII-символов парами цифр (код−32) | +| **TCP-транспорт (G2 V2 panel)** | сейчас только serial; нужен режим в `CATTcp` | +| **Настраиваемая раскладка** | сейчас зашита; вынести в конфиг/UI (как `AndromedaEditForm` в Thetis) | +| **Long-press кнопок** | принимается (`ZZZP` state=2), но действий пока нет — обрабатывается только нажатие (state=1) | +| **Индикаторы RIT/XIT/PureSignal/Diversity/ATU** | нет соответствующих функций в `TRadioController` | +| **Мультифункц. энкодер (`aeMulti`)** | назначение не задано | + +--- + +## 6. Точки расширения (чек-лист) + +- **Добавить действие кнопки/энкодера:** новый элемент в `TAndButton`/`TAndEncoder` + (AndromedaMap.pas) + ветка в `TAndromedaPanel.SyncButton`/`SyncEncoder` + (AndromedaPanel.pas) + правка таблицы `AND_BUTTON_MAP`/`AND_ENCODER_MAP`. +- **Добавить индикатор:** элемент `TAndIndicator` + номер в `AND_INDICATOR_NUM` + + push в `SyncAllIndicators` и в `OnControllerState` (по нужному `rf*`-полю). +- **Изменить раскладку:** только `AndromedaMap.pas` (таблицы-константы). +- Сверяться с форматами Thetis: `Andromeda/Andromeda.cs` (логика панели, + `EIndicatorActions`/`EButtonBarActions`/`EEncoderActions`) и + `CAT/CATCommands.cs` (`ZZZ*`).