Files
ewsdr/AndromedaPanel.pas
T
ew8bakandClaude Opus 4.8 2d7290e44d 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>
2026-06-26 11:40:10 +03:00

222 lines
9.4 KiB
ObjectPascal

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<v> → 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.