mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
Button/encoder indices follow Thetis MakeNewAndromedaDataset (our 0-based index = Thetis Number - 1), verified against andromeda_test/AndDecode.pas. The old map assumed sequential 0,1,2.. and truncated at 32; the real panel sends sparse indices up to 47. - AndromedaMap: AND_MAX_BUTTONS 32->48, renumbered button/encoder tables; new actions abVFOAtoB/abVFOBtoA/abVFOLock/abStartStop/abShift. - RadioController: VFO Lock (FVfoLock, rfVfoLock, SetVfoLock, gate in TuneActiveBy). - AndromedaPanel: wire A>B/B>A/SDR-ON/Lock; Shift band-keypad layer (#28 latch, #29..39 -> SetBand 160m..6m, non-sticky); aiVFOLock/aiShift indicators. - Filter High/Low (#4/#5) left as width approximation per decision. - doc/ANDROMEDA.md updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
267 lines
11 KiB
ObjectPascal
267 lines
11 KiB
ObjectPascal
unit AndromedaPanel;
|
|
|
|
{
|
|
AndromedaPanel.pas — логика передней панели Apache Labs Andromeda / G2.
|
|
|
|
TAndromedaPanel — «мозг» панели поверх TRadioController:
|
|
• входящие события (энкодер VFO, поворот энкодера, нажатие кнопки, версия HW)
|
|
транслируются в действия контроллера по таблицам AndromedaMap;
|
|
• изменения состояния контроллера (через AddStateListener) превращаются в
|
|
сообщения подсветки ZZZI и отдаются наружу через callback SendProc.
|
|
|
|
Архитектура: панель зависит ТОЛЬКО от TRadioController и AndromedaMap. Транспорт
|
|
(serial-порт) и связывание с CAT-движком — в CATAdapter, который создаёт панель,
|
|
даёт ей SendProc и маршрутизирует к ней callback'и движка.
|
|
|
|
Потоки: входящие хэндлеры зовутся из потока транспорта (CAT serial) и
|
|
маршалятся в поток контроллера через FController.Invoke. Слушатель состояния
|
|
и SendProc работают из потока контроллера — формирование строки и отправка в
|
|
порт потокобезопасны (serial SendStr под своим локом).
|
|
}
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
interface
|
|
|
|
uses
|
|
SysUtils, RadioController, AndromedaMap;
|
|
|
|
const
|
|
AND_VFO_STEP_HZ = 10; // Гц на один шаг энкодера VFO (как DoTuneUp/Down в CAT)
|
|
AND_BW_STEP_HZ = 50; // Гц на шаг энкодера ширины фильтра
|
|
MODE_COUNT_CYCLE = 8; // MODE_LSB..MODE_SAM (0..7) для перебора режимов
|
|
|
|
type
|
|
TAndromedaSend = procedure(const S: string) of object;
|
|
|
|
TAndromedaPanel = class
|
|
private
|
|
FController: TRadioController;
|
|
FSend: TAndromedaSend;
|
|
FConnected: Boolean; // панель прислала версию (ZZZS)
|
|
FShiftActive: Boolean; // band-keypad модификатор (latched)
|
|
FIndState: array[0..20] of Integer; // кэш состояний LED: -1 неизв., 0/1
|
|
// scratch для маршалинга в поток контроллера
|
|
FPendingVfoSteps: Integer;
|
|
FPendingEnc: TAndEncoder;
|
|
FPendingStep: Integer;
|
|
FPendingButton: TAndButton;
|
|
FPendingBand: Integer;
|
|
|
|
procedure SyncVfo;
|
|
procedure SyncEncoder;
|
|
procedure SyncButton;
|
|
procedure SyncBand;
|
|
procedure SyncShiftLed;
|
|
|
|
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);
|
|
// Раскладка зеркалит Thetis: кнопка Shift (0-based 28) — модификатор-защёлка;
|
|
// пока он активен, кнопки band-группы (29..40) выбирают диапазон напрямую
|
|
// (29→160m … 39→6m; 40=GEN в ewsdr нет). Остальные кнопки работают всегда,
|
|
// shift при их нажатии не снимается (non-sticky: снимается только band-нажатием).
|
|
var a: TAndButton;
|
|
begin
|
|
if not State then Exit; // реагируем на нажатие; отпускание/long-press — позже
|
|
a := ButtonAction(Btn);
|
|
|
|
if a = abShift then // toggle band-keypad
|
|
begin
|
|
FShiftActive := not FShiftActive;
|
|
FController.Invoke(@SyncShiftLed);
|
|
Exit;
|
|
end;
|
|
|
|
if FShiftActive and (Btn >= 29) and (Btn <= 40) then // band-keypad
|
|
begin
|
|
if (Btn >= 29) and (Btn <= 39) then // 160m..6m → SetBand 0..10
|
|
begin
|
|
FPendingBand := Btn - 29;
|
|
FController.Invoke(@SyncBand);
|
|
end;
|
|
FShiftActive := False; // non-sticky: снять shift
|
|
FController.Invoke(@SyncShiftLed);
|
|
Exit;
|
|
end;
|
|
|
|
if a = abNone then Exit;
|
|
FPendingButton := a;
|
|
FController.Invoke(@SyncButton);
|
|
end;
|
|
|
|
procedure TAndromedaPanel.SyncBand;
|
|
begin
|
|
FController.SetBand(FPendingBand);
|
|
end;
|
|
|
|
procedure TAndromedaPanel.SyncShiftLed;
|
|
begin
|
|
PushIndicator(aiShift, FShiftActive);
|
|
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);
|
|
abVFOAtoB: FController.VfoACopyB;
|
|
abVFOBtoA: FController.VfoBCopyA;
|
|
abVFOLock: FController.SetVfoLock(not FController.FVfoLock);
|
|
abStartStop: FController.StartStop;
|
|
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(aiVFOLock, FController.FVfoLock);
|
|
PushIndicator(aiShift, FShiftActive);
|
|
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);
|
|
rfVfoLock: PushIndicator(aiVFOLock, FController.FVfoLock);
|
|
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.
|