mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
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:
@@ -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.
|
||||
@@ -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<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.
|
||||
+34
-1
@@ -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.
|
||||
|
||||
+73
-5
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+21
-3
@@ -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);
|
||||
|
||||
@@ -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<v>` | панель сообщает версию HW/SW (рукопожатие) |
|
||||
| | `ZZZU<nn>` / `ZZZD<nn>` | энкодер VFO повёрнут вверх/вниз на nn шагов |
|
||||
| | `ZZZE<eees>` | энкодер #ee повёрнут (01–20 = +шаг, 51–70 = −шаг) |
|
||||
| | `ZZZP<bbs>` | кнопка #bb: s=1 нажата, 2 long-press, 0 отпущена |
|
||||
| **Хост → панель** | `ZZZI<nn><s>` | 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*`).
|
||||
Reference in New Issue
Block a user