Files
ewsdr/AndromedaPanel.pas

268 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, RadioModes;
const
AND_VFO_STEP_HZ = 10; // Гц на один шаг энкодера VFO (как DoTuneUp/Down в CAT)
AND_BW_STEP_HZ = 50; // Гц на шаг энкодера ширины фильтра
// Аппаратная панель намеренно перебирает только LSB..SAM.
MODE_COUNT_CYCLE = RadioModes.MODE_SAM - RadioModes.MODE_LSB + 1;
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.