feat(cat): Andromeda/G2 front panel support (serial, LED indicators)

Bidirectional Apache Labs Andromeda/G2 front-panel protocol on top of CAT.
Panel logic lives in dedicated units; existing files get only thin hooks.

New units:
- AndromedaMap.pas   — data only: action enums + default button/encoder
  layout tables and LED (ZZZI) numbers (mirroring the Thetis defaults)
- AndromedaPanel.pas — TAndromedaPanel: inbound panel events -> controller
  actions (marshalled via Invoke), state subscription -> ZZZI push via SendProc

Hooks:
- CATEngine: parse ZZZD/ZZZU/ZZZE/ZZZP/ZZZS into generic TCATContext callbacks
- CATAdapter: creates the panel, routes engine callbacks to it, supplies the
  SendProc to the Andromeda port, AddStateListener, ZZZS handshake
- CATSerial: Andromeda flag in port config + HasAndromeda/SendToAndromeda
- RadioController: AddStateListener (multicast, does not steal MainForm's
  OnStateChanged)
- Settings/SettingsForm/MainForm: CATSerialAndromeda[0..3] + persistence + checkbox

First version: serial transport, hardcoded default mapping, LED indicators
(MOX/Tune/CTUN/VFO A-B/Split/NB/NR/SNB/ANF/Squelch). Doc: doc/ANDROMEDA.md.

TODO: default button mapping does not match the physical panel (hardcoded) —
to be fixed later; also ZZMF/LCD text, TCP G2V2 transport, configurable mapping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 11:40:10 +03:00
co-authored by Claude Opus 4.8
parent 4a92624e9b
commit 2d7290e44d
10 changed files with 704 additions and 9 deletions
+73 -5
View File
@@ -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;