mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:27:33 +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
|
uses
|
||||||
Classes, SysUtils,
|
Classes, SysUtils,
|
||||||
RadioController, CATEngine, CATSerial, CATTcp, Settings, WDSPEngine;
|
RadioController, CATEngine, CATSerial, CATTcp, Settings, WDSPEngine,
|
||||||
|
AndromedaPanel;
|
||||||
|
|
||||||
type
|
type
|
||||||
TCATAdapter = class
|
TCATAdapter = class
|
||||||
@@ -29,6 +30,7 @@ type
|
|||||||
FEngine: TCATEngine;
|
FEngine: TCATEngine;
|
||||||
FSerial: TCATSerialManager;
|
FSerial: TCATSerialManager;
|
||||||
FTcp: TCATTcpServer;
|
FTcp: TCATTcpServer;
|
||||||
|
FPanel: TAndromedaPanel; // передняя панель Andromeda/G2 (если включена)
|
||||||
// Параметры команды для маршалинга в поток контроллера (через Invoke).
|
// Параметры команды для маршалинга в поток контроллера (через Invoke).
|
||||||
FSyncFreq: Double;
|
FSyncFreq: Double;
|
||||||
FSyncInt: Integer;
|
FSyncInt: Integer;
|
||||||
@@ -93,6 +95,12 @@ type
|
|||||||
procedure SetAGCTop(V: Integer);
|
procedure SetAGCTop(V: Integer);
|
||||||
procedure SetCTun(V: Boolean);
|
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-методы (выполняются в потоке контроллера) ──
|
// ── Sync-методы (выполняются в потоке контроллера) ──
|
||||||
procedure SyncVfoA;
|
procedure SyncVfoA;
|
||||||
procedure SyncVfoB;
|
procedure SyncVfoB;
|
||||||
@@ -194,10 +202,18 @@ begin
|
|||||||
Ctx.SetAGCTop := @SetAGCTop;
|
Ctx.SetAGCTop := @SetAGCTop;
|
||||||
Ctx.GetCTun := @GetCTun;
|
Ctx.GetCTun := @GetCTun;
|
||||||
Ctx.SetCTun := @SetCTun;
|
Ctx.SetCTun := @SetCTun;
|
||||||
|
Ctx.OnPanelVfoStep := @PanelVfoStep;
|
||||||
|
Ctx.OnPanelEncoder := @PanelEncoder;
|
||||||
|
Ctx.OnPanelButton := @PanelButton;
|
||||||
|
Ctx.OnPanelVersion := @PanelVersion;
|
||||||
|
|
||||||
FEngine := TCATEngine.Create(Ctx);
|
FEngine := TCATEngine.Create(Ctx);
|
||||||
FSerial := TCATSerialManager.Create(FEngine);
|
FSerial := TCATSerialManager.Create(FEngine);
|
||||||
FTcp := TCATTcpServer.Create(FEngine);
|
FTcp := TCATTcpServer.Create(FEngine);
|
||||||
|
|
||||||
|
// Передняя панель: отправляет наружу через Andromeda-порт, слушает состояние.
|
||||||
|
FPanel := TAndromedaPanel.Create(FController, @FSerial.SendToAndromeda);
|
||||||
|
FController.AddStateListener(@FPanel.OnControllerState);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
destructor TCATAdapter.Destroy;
|
destructor TCATAdapter.Destroy;
|
||||||
@@ -205,6 +221,7 @@ begin
|
|||||||
if Assigned(FTcp) then begin FTcp.Stop; FreeAndNil(FTcp); end;
|
if Assigned(FTcp) then begin FTcp.Stop; FreeAndNil(FTcp); end;
|
||||||
if Assigned(FSerial) then begin FSerial.StopAll; FreeAndNil(FSerial); end;
|
if Assigned(FSerial) then begin FSerial.StopAll; FreeAndNil(FSerial); end;
|
||||||
FreeAndNil(FEngine);
|
FreeAndNil(FEngine);
|
||||||
|
FreeAndNil(FPanel);
|
||||||
inherited Destroy;
|
inherited Destroy;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -225,8 +242,11 @@ begin
|
|||||||
2: Cfgs[i].Parity := cspEven;
|
2: Cfgs[i].Parity := cspEven;
|
||||||
else Cfgs[i].Parity := cspNone;
|
else Cfgs[i].Parity := cspNone;
|
||||||
end;
|
end;
|
||||||
|
Cfgs[i].Andromeda := G.CATSerialAndromeda[i];
|
||||||
end;
|
end;
|
||||||
FSerial.ApplyConfig(Cfgs);
|
FSerial.ApplyConfig(Cfgs);
|
||||||
|
// Если назначен Andromeda-порт — запросить версию панели (рукопожатие ZZZS).
|
||||||
|
if FSerial.HasAndromeda and Assigned(FPanel) then FPanel.RequestVersion;
|
||||||
FTcp.Stop;
|
FTcp.Stop;
|
||||||
if G.CATTcpEnabled then
|
if G.CATTcpEnabled then
|
||||||
begin
|
begin
|
||||||
@@ -442,4 +462,17 @@ begin FSyncBool := V; FController.Invoke(@SyncCTun); end;
|
|||||||
procedure TCATAdapter.SyncCTun;
|
procedure TCATAdapter.SyncCTun;
|
||||||
begin FController.SetCTun(FSyncBool); end;
|
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.
|
end.
|
||||||
|
|||||||
+73
-5
@@ -48,6 +48,9 @@ type
|
|||||||
TCATSetIntProc = procedure(V: Integer) of object;
|
TCATSetIntProc = procedure(V: Integer) of object;
|
||||||
TCATSetBoolProc = procedure(V: Boolean) of object;
|
TCATSetBoolProc = procedure(V: Boolean) of object;
|
||||||
TCATProc = procedure 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
|
TCATContext = record
|
||||||
GetVfoA: TCATGetDoubleFunc;
|
GetVfoA: TCATGetDoubleFunc;
|
||||||
@@ -106,6 +109,11 @@ type
|
|||||||
SetAGCTop: TCATSetIntProc; // AGC-T порог, dB
|
SetAGCTop: TCATSetIntProc; // AGC-T порог, dB
|
||||||
GetCTun: TCATGetBoolFunc; // Click-tune (CTUN) вкл/выкл
|
GetCTun: TCATGetBoolFunc; // Click-tune (CTUN) вкл/выкл
|
||||||
SetCTun: TCATSetBoolProc;
|
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;
|
end;
|
||||||
|
|
||||||
TCATEngine = class
|
TCATEngine = class
|
||||||
@@ -321,6 +329,8 @@ type
|
|||||||
function ZZYR(const s: string): string;
|
function ZZYR(const s: string): string;
|
||||||
function ZZZA(const s: string): string;
|
function ZZZA(const s: string): string;
|
||||||
function ZZZB: string;
|
function ZZZB: string;
|
||||||
|
function ZZZD(const s: string): string;
|
||||||
|
function ZZZE(const s: string): string;
|
||||||
function ZZZM(const s: string): string;
|
function ZZZM(const s: string): string;
|
||||||
function ZZZP(const s: string): string;
|
function ZZZP(const s: string): string;
|
||||||
function ZZZQ(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 = 'YR' then ans := ZZYR(s)
|
||||||
else if ext = 'ZA' then ans := ZZZA(s)
|
else if ext = 'ZA' then ans := ZZZA(s)
|
||||||
else if ext = 'ZB' then begin ZZZB; ans := ''; end
|
else if ext = 'ZB' then begin ZZZB; ans := ''; end
|
||||||
else if ext = 'ZD' then ans := '' // STUB: system D
|
else if ext = 'ZD' then ans := ZZZD(s)
|
||||||
else if ext = 'ZE' then ans := '' // STUB: system E
|
else if ext = 'ZE' then ans := ZZZE(s)
|
||||||
else if ext = 'ZM' then ans := ZZZM(s)
|
else if ext = 'ZM' then ans := ZZZM(s)
|
||||||
else if ext = 'ZN' then ans := '' // STUB: system N
|
else if ext = 'ZN' then ans := '' // STUB: system N
|
||||||
else if ext = 'ZO' then ans := ZZZO(s)
|
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;
|
function TCATEngine.ZZZA(const s: string): string;
|
||||||
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||||
function TCATEngine.ZZZB: string; begin Result := ''; 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;
|
function TCATEngine.ZZZM(const s: string): string;
|
||||||
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||||
|
|
||||||
function TCATEngine.ZZZP(const s: string): string;
|
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;
|
function TCATEngine.ZZZQ(const s: string): string;
|
||||||
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||||
function TCATEngine.ZZZR(const s: string): string;
|
function TCATEngine.ZZZR(const s: string): string;
|
||||||
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||||
function TCATEngine.ZZZS(const s: string): string;
|
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;
|
function TCATEngine.ZZZT(const s: string): string;
|
||||||
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||||
function TCATEngine.ZZZU(const s: string): string;
|
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;
|
function TCATEngine.ZZZV(const s: string): string;
|
||||||
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
begin if Length(s) > 0 then Result := '' else Result := '0'; end;
|
||||||
function TCATEngine.ZZZW(const s: string): string;
|
function TCATEngine.ZZZW(const s: string): string;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ type
|
|||||||
DataBits: Integer; // 7 or 8
|
DataBits: Integer; // 7 or 8
|
||||||
StopBits: Integer; // 1 or 2
|
StopBits: Integer; // 1 or 2
|
||||||
Parity: TCATSerialParity;
|
Parity: TCATSerialParity;
|
||||||
|
Andromeda: Boolean; // порт — передняя панель Andromeda/G2 (push ZZZI наружу)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{$IFDEF CAT_SERIAL_STUB}
|
{$IFDEF CAT_SERIAL_STUB}
|
||||||
@@ -95,12 +96,16 @@ type
|
|||||||
private
|
private
|
||||||
FPorts: array[0..CAT_SERIAL_MAX_PORTS-1] of TCATSerialPort;
|
FPorts: array[0..CAT_SERIAL_MAX_PORTS-1] of TCATSerialPort;
|
||||||
FEngine: TCATEngine;
|
FEngine: TCATEngine;
|
||||||
|
FAndromedaPort: TCATSerialPort; // первый активный порт с Andromeda=true (или nil)
|
||||||
public
|
public
|
||||||
constructor Create(AEngine: TCATEngine);
|
constructor Create(AEngine: TCATEngine);
|
||||||
destructor Destroy; override;
|
destructor Destroy; override;
|
||||||
procedure ApplyConfig(const Configs: array of TCATSerialConfig);
|
procedure ApplyConfig(const Configs: array of TCATSerialConfig);
|
||||||
procedure StopAll;
|
procedure StopAll;
|
||||||
function ActiveCount: Integer;
|
function ActiveCount: Integer;
|
||||||
|
// Передняя панель: есть ли активный Andromeda-порт и отправка строки в него.
|
||||||
|
function HasAndromeda: Boolean;
|
||||||
|
procedure SendToAndromeda(const S: string); // сигнатура совместима с TAndromedaSend
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
@@ -267,6 +272,7 @@ var i: Integer;
|
|||||||
begin
|
begin
|
||||||
inherited Create;
|
inherited Create;
|
||||||
FEngine := AEngine;
|
FEngine := AEngine;
|
||||||
|
FAndromedaPort := nil;
|
||||||
for i := 0 to CAT_SERIAL_MAX_PORTS-1 do
|
for i := 0 to CAT_SERIAL_MAX_PORTS-1 do
|
||||||
FPorts[i] := nil;
|
FPorts[i] := nil;
|
||||||
end;
|
end;
|
||||||
@@ -288,12 +294,25 @@ begin
|
|||||||
for i := 0 to cnt-1 do begin
|
for i := 0 to cnt-1 do begin
|
||||||
FPorts[i] := TCATSerialPort.Create(FEngine, Configs[i]);
|
FPorts[i] := TCATSerialPort.Create(FEngine, Configs[i]);
|
||||||
if Configs[i].Enabled then FPorts[i].Start;
|
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;
|
||||||
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;
|
procedure TCATSerialManager.StopAll;
|
||||||
var i: Integer;
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
|
FAndromedaPort := nil; // указывает в FPorts[] — обнулить ДО освобождения
|
||||||
for i := 0 to CAT_SERIAL_MAX_PORTS-1 do begin
|
for i := 0 to CAT_SERIAL_MAX_PORTS-1 do begin
|
||||||
if Assigned(FPorts[i]) then begin
|
if Assigned(FPorts[i]) then begin
|
||||||
FPorts[i].Stop;
|
FPorts[i].Stop;
|
||||||
|
|||||||
@@ -476,6 +476,7 @@ type
|
|||||||
const SerEnabled: array of Boolean;
|
const SerEnabled: array of Boolean;
|
||||||
const SerPort: array of string;
|
const SerPort: array of string;
|
||||||
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
|
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
|
||||||
|
const SerAndromeda: array of Boolean;
|
||||||
TcpEnabled: Boolean; TcpPort: Integer);
|
TcpEnabled: Boolean; TcpPort: Integer);
|
||||||
procedure OnAlexSettingsChange(const A: TAlexSettings);
|
procedure OnAlexSettingsChange(const A: TAlexSettings);
|
||||||
procedure OnXvtrSettingsChange(const X: TXvtrSettings);
|
procedure OnXvtrSettingsChange(const X: TXvtrSettings);
|
||||||
@@ -5872,6 +5873,7 @@ begin
|
|||||||
FCATLastGlobal.CATSerialDataBits,
|
FCATLastGlobal.CATSerialDataBits,
|
||||||
FCATLastGlobal.CATSerialStopBits,
|
FCATLastGlobal.CATSerialStopBits,
|
||||||
FCATLastGlobal.CATSerialParity,
|
FCATLastGlobal.CATSerialParity,
|
||||||
|
FCATLastGlobal.CATSerialAndromeda,
|
||||||
FCATLastGlobal.CATTcpEnabled,
|
FCATLastGlobal.CATTcpEnabled,
|
||||||
FCATLastGlobal.CATTcpPort);
|
FCATLastGlobal.CATTcpPort);
|
||||||
// Перечисляем PA устройства
|
// Перечисляем PA устройства
|
||||||
@@ -5940,6 +5942,7 @@ procedure TMainForm.OnCATSettingsChange(
|
|||||||
const SerEnabled: array of Boolean;
|
const SerEnabled: array of Boolean;
|
||||||
const SerPort: array of string;
|
const SerPort: array of string;
|
||||||
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
|
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
|
||||||
|
const SerAndromeda: array of Boolean;
|
||||||
TcpEnabled: Boolean; TcpPort: Integer);
|
TcpEnabled: Boolean; TcpPort: Integer);
|
||||||
var i: Integer;
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
@@ -5951,6 +5954,7 @@ begin
|
|||||||
if i <= High(SerDataBits) then FCATLastGlobal.CATSerialDataBits[i] := SerDataBits[i];
|
if i <= High(SerDataBits) then FCATLastGlobal.CATSerialDataBits[i] := SerDataBits[i];
|
||||||
if i <= High(SerStopBits) then FCATLastGlobal.CATSerialStopBits[i] := SerStopBits[i];
|
if i <= High(SerStopBits) then FCATLastGlobal.CATSerialStopBits[i] := SerStopBits[i];
|
||||||
if i <= High(SerParity) then FCATLastGlobal.CATSerialParity[i] := SerParity[i];
|
if i <= High(SerParity) then FCATLastGlobal.CATSerialParity[i] := SerParity[i];
|
||||||
|
if i <= High(SerAndromeda)then FCATLastGlobal.CATSerialAndromeda[i]:= SerAndromeda[i];
|
||||||
end;
|
end;
|
||||||
FCATLastGlobal.CATTcpEnabled := TcpEnabled;
|
FCATLastGlobal.CATTcpEnabled := TcpEnabled;
|
||||||
FCATLastGlobal.CATTcpPort := TcpPort;
|
FCATLastGlobal.CATTcpPort := TcpPort;
|
||||||
@@ -5962,6 +5966,7 @@ begin
|
|||||||
FController.FLoadedGlobal.CATSerialDataBits := FCATLastGlobal.CATSerialDataBits;
|
FController.FLoadedGlobal.CATSerialDataBits := FCATLastGlobal.CATSerialDataBits;
|
||||||
FController.FLoadedGlobal.CATSerialStopBits := FCATLastGlobal.CATSerialStopBits;
|
FController.FLoadedGlobal.CATSerialStopBits := FCATLastGlobal.CATSerialStopBits;
|
||||||
FController.FLoadedGlobal.CATSerialParity := FCATLastGlobal.CATSerialParity;
|
FController.FLoadedGlobal.CATSerialParity := FCATLastGlobal.CATSerialParity;
|
||||||
|
FController.FLoadedGlobal.CATSerialAndromeda := FCATLastGlobal.CATSerialAndromeda;
|
||||||
FController.FLoadedGlobal.CATTcpEnabled := FCATLastGlobal.CATTcpEnabled;
|
FController.FLoadedGlobal.CATTcpEnabled := FCATLastGlobal.CATTcpEnabled;
|
||||||
FController.FLoadedGlobal.CATTcpPort := FCATLastGlobal.CATTcpPort;
|
FController.FLoadedGlobal.CATTcpPort := FCATLastGlobal.CATTcpPort;
|
||||||
FCATAdapter.ApplySettings(FCATLastGlobal);
|
FCATAdapter.ApplySettings(FCATLastGlobal);
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ type
|
|||||||
private
|
private
|
||||||
FOnInvoke: TUIInvokeEvent;
|
FOnInvoke: TUIInvokeEvent;
|
||||||
FOnStateChanged: TRadioStateEvent;
|
FOnStateChanged: TRadioStateEvent;
|
||||||
|
FStateListeners: array of TRadioStateEvent; // доп. наблюдатели (CAT/Andromeda), помимо FOnStateChanged (MainForm)
|
||||||
FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA
|
FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA
|
||||||
FOnAudioConsume: TAudioConsumeEvent; // web-перехват RX-аудио (если активен клиент)
|
FOnAudioConsume: TAudioConsumeEvent; // web-перехват RX-аудио (если активен клиент)
|
||||||
FLocalAudio: Boolean; // локальный звук (динамик/мик ПК): GUI=True, демон=False
|
FLocalAudio: Boolean; // локальный звук (динамик/мик ПК): GUI=True, демон=False
|
||||||
@@ -577,6 +578,9 @@ type
|
|||||||
// ---- События наружу ----
|
// ---- События наружу ----
|
||||||
property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke;
|
property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke;
|
||||||
property OnStateChanged: TRadioStateEvent read FOnStateChanged write FOnStateChanged;
|
property OnStateChanged: TRadioStateEvent read FOnStateChanged write FOnStateChanged;
|
||||||
|
// Доп. наблюдатели состояния (помимо единственного OnStateChanged, который
|
||||||
|
// занимает MainForm). Нужны CAT/Andromeda для push индикаторов панели.
|
||||||
|
procedure AddStateListener(L: TRadioStateEvent);
|
||||||
// Пост-тюн хук: вызывается в КОНЦЕ SetVfoA (после Changed(rfVfoA), т.е. вне
|
// Пост-тюн хук: вызывается в КОНЦЕ SetVfoA (после Changed(rfVfoA), т.е. вне
|
||||||
// рендера) — сюда вешается канальная оркестрация (ChannelController.OnVfoTuned),
|
// рендера) — сюда вешается канальная оркестрация (ChannelController.OnVfoTuned),
|
||||||
// чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless.
|
// чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless.
|
||||||
@@ -676,8 +680,18 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.Changed(Field: TRadioField);
|
procedure TRadioController.Changed(Field: TRadioField);
|
||||||
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
if Assigned(FOnStateChanged) then FOnStateChanged(Self, Field);
|
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;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.PushNetworkState;
|
procedure TRadioController.PushNetworkState;
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ type
|
|||||||
CATSerialDataBits: array[0..3] of Integer; // 7 или 8
|
CATSerialDataBits: array[0..3] of Integer; // 7 или 8
|
||||||
CATSerialStopBits: array[0..3] of Integer; // 1 или 2
|
CATSerialStopBits: array[0..3] of Integer; // 1 или 2
|
||||||
CATSerialParity: array[0..3] of Integer; // 0=None, 1=Odd, 2=Even
|
CATSerialParity: array[0..3] of Integer; // 0=None, 1=Odd, 2=Even
|
||||||
|
CATSerialAndromeda:array[0..3] of Boolean; // порт — передняя панель Andromeda/G2
|
||||||
// TCP CAT сервер (один, многоклиентский)
|
// TCP CAT сервер (один, многоклиентский)
|
||||||
CATTcpEnabled: Boolean;
|
CATTcpEnabled: Boolean;
|
||||||
CATTcpPort: Integer; // порт, по умолчанию 19090
|
CATTcpPort: Integer; // порт, по умолчанию 19090
|
||||||
@@ -464,6 +465,7 @@ begin
|
|||||||
G.CATSerialDataBits[i] := 8;
|
G.CATSerialDataBits[i] := 8;
|
||||||
G.CATSerialStopBits[i] := 1;
|
G.CATSerialStopBits[i] := 1;
|
||||||
G.CATSerialParity[i] := 0; // None
|
G.CATSerialParity[i] := 0; // None
|
||||||
|
G.CATSerialAndromeda[i]:= False;
|
||||||
end;
|
end;
|
||||||
G.CATTcpEnabled := False;
|
G.CATTcpEnabled := False;
|
||||||
G.CATTcpPort := 19090;
|
G.CATTcpPort := 19090;
|
||||||
@@ -696,6 +698,7 @@ begin
|
|||||||
G.CATSerialDataBits[i] := JI(GObj,'cat_serial_dbits_'+IntToStr(i), 8);
|
G.CATSerialDataBits[i] := JI(GObj,'cat_serial_dbits_'+IntToStr(i), 8);
|
||||||
G.CATSerialStopBits[i] := JI(GObj,'cat_serial_sbits_'+IntToStr(i), 1);
|
G.CATSerialStopBits[i] := JI(GObj,'cat_serial_sbits_'+IntToStr(i), 1);
|
||||||
G.CATSerialParity[i] := JI(GObj,'cat_serial_parity_'+IntToStr(i), 0);
|
G.CATSerialParity[i] := JI(GObj,'cat_serial_parity_'+IntToStr(i), 0);
|
||||||
|
G.CATSerialAndromeda[i]:= JB(GObj,'cat_serial_androm_'+IntToStr(i), False);
|
||||||
end;
|
end;
|
||||||
G.CATTcpEnabled := JB(GObj,'cat_tcp_enabled', False);
|
G.CATTcpEnabled := JB(GObj,'cat_tcp_enabled', False);
|
||||||
G.CATTcpPort := JI(GObj,'cat_tcp_port', 19090);
|
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_dbits_'+IntToStr(i),G.CATSerialDataBits[i]);
|
||||||
JW(O,'cat_serial_sbits_'+IntToStr(i),G.CATSerialStopBits[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_parity_'+IntToStr(i),G.CATSerialParity[i]);
|
||||||
|
JW(O,'cat_serial_androm_'+IntToStr(i),G.CATSerialAndromeda[i]);
|
||||||
end;
|
end;
|
||||||
JW(O,'cat_tcp_enabled',G.CATTcpEnabled);
|
JW(O,'cat_tcp_enabled',G.CATTcpEnabled);
|
||||||
JW(O,'cat_tcp_port',G.CATTcpPort);
|
JW(O,'cat_tcp_port',G.CATTcpPort);
|
||||||
@@ -932,6 +936,7 @@ begin
|
|||||||
JW(O, 'ser_dbits_' +IntToStr(i), G.CATSerialDataBits[i]);
|
JW(O, 'ser_dbits_' +IntToStr(i), G.CATSerialDataBits[i]);
|
||||||
JW(O, 'ser_sbits_' +IntToStr(i), G.CATSerialStopBits[i]);
|
JW(O, 'ser_sbits_' +IntToStr(i), G.CATSerialStopBits[i]);
|
||||||
JW(O, 'ser_parity_'+IntToStr(i), G.CATSerialParity[i]);
|
JW(O, 'ser_parity_'+IntToStr(i), G.CATSerialParity[i]);
|
||||||
|
JW(O, 'ser_androm_'+IntToStr(i), G.CATSerialAndromeda[i]);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -954,6 +959,7 @@ begin
|
|||||||
G.CATSerialDataBits[i] := JI(O, 'ser_dbits_' +IntToStr(i), 8);
|
G.CATSerialDataBits[i] := JI(O, 'ser_dbits_' +IntToStr(i), 8);
|
||||||
G.CATSerialStopBits[i] := JI(O, 'ser_sbits_' +IntToStr(i), 1);
|
G.CATSerialStopBits[i] := JI(O, 'ser_sbits_' +IntToStr(i), 1);
|
||||||
G.CATSerialParity[i] := JI(O, 'ser_parity_'+IntToStr(i), 0);
|
G.CATSerialParity[i] := JI(O, 'ser_parity_'+IntToStr(i), 0);
|
||||||
|
G.CATSerialAndromeda[i]:= JB(O, 'ser_androm_'+IntToStr(i), False);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
+21
-3
@@ -67,6 +67,7 @@ type
|
|||||||
const SerDataBits: array of Integer;
|
const SerDataBits: array of Integer;
|
||||||
const SerStopBits: array of Integer;
|
const SerStopBits: array of Integer;
|
||||||
const SerParity: array of Integer;
|
const SerParity: array of Integer;
|
||||||
|
const SerAndromeda: array of Boolean;
|
||||||
TcpEnabled: Boolean; TcpPort: Integer) of object;
|
TcpEnabled: Boolean; TcpPort: Integer) of object;
|
||||||
TOnAlexChange = procedure(const A: TAlexSettings) of object;
|
TOnAlexChange = procedure(const A: TAlexSettings) of object;
|
||||||
TOnXvtrChange = procedure(const X: TXvtrSettings) of object;
|
TOnXvtrChange = procedure(const X: TXvtrSettings) of object;
|
||||||
@@ -263,6 +264,7 @@ type
|
|||||||
FCATSerialDataBits: array[0..3] of TFlatComboBox;
|
FCATSerialDataBits: array[0..3] of TFlatComboBox;
|
||||||
FCATSerialStopBits: array[0..3] of TFlatComboBox;
|
FCATSerialStopBits: array[0..3] of TFlatComboBox;
|
||||||
FCATSerialParity: array[0..3] of TFlatComboBox;
|
FCATSerialParity: array[0..3] of TFlatComboBox;
|
||||||
|
FCATSerialAndromeda:array[0..3] of TFlatCheckBox;
|
||||||
FCATTcpEn: TFlatCheckBox;
|
FCATTcpEn: TFlatCheckBox;
|
||||||
FCATTcpPort: TFlatSpinEdit;
|
FCATTcpPort: TFlatSpinEdit;
|
||||||
|
|
||||||
@@ -415,6 +417,7 @@ type
|
|||||||
const SerEnabled: array of Boolean;
|
const SerEnabled: array of Boolean;
|
||||||
const SerPort: array of string;
|
const SerPort: array of string;
|
||||||
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
|
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
|
||||||
|
const SerAndromeda: array of Boolean;
|
||||||
TcpEnabled: Boolean; TcpPort: Integer);
|
TcpEnabled: Boolean; TcpPort: Integer);
|
||||||
procedure LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0);
|
procedure LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0);
|
||||||
procedure LoadAlexSettings(const A: TAlexSettings; BoardType: Integer);
|
procedure LoadAlexSettings(const A: TAlexSettings; BoardType: Integer);
|
||||||
@@ -2621,7 +2624,7 @@ const
|
|||||||
LW = 120;
|
LW = 120;
|
||||||
CX = PAD + LW + 8;
|
CX = PAD + LW + 8;
|
||||||
GRP_W = 320;
|
GRP_W = 320;
|
||||||
GRP_H = 240;
|
GRP_H = 272;
|
||||||
GRP_GAP = 16;
|
GRP_GAP = 16;
|
||||||
CMB_W = 140;
|
CMB_W = 140;
|
||||||
EDT_W = 160;
|
EDT_W = 160;
|
||||||
@@ -2712,6 +2715,16 @@ begin
|
|||||||
Cmb.Items.Add('None'); Cmb.Items.Add('Odd'); Cmb.Items.Add('Even');
|
Cmb.Items.Add('None'); Cmb.Items.Add('Odd'); Cmb.Items.Add('Even');
|
||||||
Cmb.ItemIndex := 0; // None
|
Cmb.ItemIndex := 0; // None
|
||||||
FCATSerialParity[i] := Cmb;
|
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;
|
end;
|
||||||
|
|
||||||
// TCP server group below the two rows of serial port panels
|
// TCP server group below the two rows of serial port panels
|
||||||
@@ -2747,6 +2760,7 @@ var
|
|||||||
SerEn: array[0..3] of Boolean;
|
SerEn: array[0..3] of Boolean;
|
||||||
SerPort: array[0..3] of string;
|
SerPort: array[0..3] of string;
|
||||||
SerBaud, SerData, SerStop, SerPar: array[0..3] of Integer;
|
SerBaud, SerData, SerStop, SerPar: array[0..3] of Integer;
|
||||||
|
SerAndr: array[0..3] of Boolean;
|
||||||
i, Idx: Integer;
|
i, Idx: Integer;
|
||||||
begin
|
begin
|
||||||
if FLoading then Exit;
|
if FLoading then Exit;
|
||||||
@@ -2762,9 +2776,10 @@ begin
|
|||||||
SerBaud[i] := 9600;
|
SerBaud[i] := 9600;
|
||||||
if FCATSerialDataBits[i].ItemIndex = 0 then SerData[i] := 7 else SerData[i] := 8;
|
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;
|
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;
|
end;
|
||||||
FOnCATChange(SerEn, SerPort, SerBaud, SerData, SerStop, SerPar,
|
FOnCATChange(SerEn, SerPort, SerBaud, SerData, SerStop, SerPar, SerAndr,
|
||||||
FCATTcpEn.Checked, FCATTcpPort.Value);
|
FCATTcpEn.Checked, FCATTcpPort.Value);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -2782,6 +2797,7 @@ procedure TSettingsForm.LoadCATSettings(
|
|||||||
const SerEnabled: array of Boolean;
|
const SerEnabled: array of Boolean;
|
||||||
const SerPort: array of string;
|
const SerPort: array of string;
|
||||||
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
|
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
|
||||||
|
const SerAndromeda: array of Boolean;
|
||||||
TcpEnabled: Boolean; TcpPort: Integer);
|
TcpEnabled: Boolean; TcpPort: Integer);
|
||||||
var
|
var
|
||||||
i, j, Idx: Integer;
|
i, j, Idx: Integer;
|
||||||
@@ -2811,6 +2827,8 @@ begin
|
|||||||
end;
|
end;
|
||||||
if i <= High(SerParity) then
|
if i <= High(SerParity) then
|
||||||
FCATSerialParity[i].ItemIndex := EnsureRange(SerParity[i], 0, 2);
|
FCATSerialParity[i].ItemIndex := EnsureRange(SerParity[i], 0, 2);
|
||||||
|
if i <= High(SerAndromeda) then
|
||||||
|
FCATSerialAndromeda[i].Checked := SerAndromeda[i];
|
||||||
end;
|
end;
|
||||||
FCATTcpEn.Checked := TcpEnabled;
|
FCATTcpEn.Checked := TcpEnabled;
|
||||||
FCATTcpPort.Value := EnsureRange(TcpPort, 1, 65535);
|
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