{
  Copyright (C)
  2026 - Uladzimir Karpenka, EW8BAK

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version 2
  of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
}

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);
    // Края независимы: крутим один, второй стоит. Контроллер уводит правку в
    // VAR, поэтому пресеты энкодером не портятся.
    aeFilterHigh:  FController.SetFilterEdges(FController.FilterLo,
                     FController.FilterHi + FPendingStep * AND_BW_STEP_HZ);
    aeFilterLow:   FController.SetFilterEdges(
                     FController.FilterLo + FPendingStep * AND_BW_STEP_HZ,
                     FController.FilterHi);
    // aeMulti — мультифункц. энкодер: пока не назначен
  end;
end;

procedure TAndromedaPanel.HandleButton(Btn: Integer; State, LongPress: Boolean);
// Кнопка 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.
