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 → 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); // Раскладка зеркалит 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.