From 0e550dd9f996d574930b6fc0bbc5825aa7aae168 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 29 Jun 2026 16:11:12 +0300 Subject: [PATCH] fix(cat): align Andromeda panel mapping with real G2 hardware (Thetis) 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 --- AndromedaMap.pas | 106 +++++++++++++++++++++++++++++++------------- AndromedaPanel.pas | 45 +++++++++++++++++++ RadioController.pas | 14 +++++- doc/ANDROMEDA.md | 67 +++++++++++++++++++--------- 4 files changed, 179 insertions(+), 53 deletions(-) diff --git a/AndromedaMap.pas b/AndromedaMap.pas index 45ad88d..33ad70d 100644 --- a/AndromedaMap.pas +++ b/AndromedaMap.pas @@ -24,7 +24,12 @@ type TAndButton = ( abNone, abMOX, abTune, abBandUp, abBandDown, abModeUp, abModeDown, abMute, abNB, abNR, abSNB, abANF, abVFOSplit, abVFOSwap, abCTUN, - abCTCSS, abSquelch, abFilterUp, abFilterDown); + abCTCSS, abSquelch, abFilterUp, abFilterDown, + abVFOAtoB, // копировать VFO A → B (Thetis eBBVFOAtoB) + abVFOBtoA, // копировать VFO B → A (Thetis eBBVFOBtoA) + abVFOLock, // блокировка перестройки VFO (Thetis eBBVFOLockOnOff) + abStartStop, // старт/стоп SDR (Thetis eBBStartStop) + abShift); // модификатор band-keypad (Thetis eBBShift) — обрабатывается в панели // Параметр, управляемый поворотным энкодером панели. TAndEncoder = ( @@ -39,43 +44,82 @@ type const // Размеры дефолтных таблиц (кнопки/энкодеры нумеруются с 0 после разбора ZZZP/ZZZE). - AND_MAX_BUTTONS = 32; + AND_MAX_BUTTONS = 48; AND_MAX_ENCODERS = 20; // Раскладка кнопок: индекс = номер кнопки (0-based), значение = действие. - // Неназначенные → abNone. Дефолт ewsdr (правится при появлении конкретной панели). + // Нумерация совпадает с дефолтом Thetis (MakeNewAndromedaDataset): наш 0-based + // индекс = «Pushbutton Number» Thetis − 1. Проверено на реальной панели + // (см. andromeda_test/AndDecode.pas). Неназначенные / отсутствующий тракт → abNone. 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); + abMute, // 0 RX1 Mute + abNone, // 1 + abNone, // 2 RX2 Mute — нет второго приёмника + abNone, // 3 + abNone, // 4 + abNone, // 5 + abNone, // 6 + abNone, // 7 + abNone, // 8 + abNone, // 9 + abNone, // 10 + abNone, // 11 + abNone, // 12 + abNone, // 13 + abNone, // 14 + abNone, // 15 + abNone, // 16 + abNone, // 17 + abNone, // 18 + abNone, // 19 + abNone, // 20 + abNone, // 21 + abNone, // 22 + abNone, // 23 + abNone, // 24 + abNone, // 25 + abNone, // 26 + abNone, // 27 + abShift, // 28 Shift (band-keypad модификатор) + abBandUp, // 29 Band + (под shift → 160m) + abModeUp, // 30 Mode + (под shift → 80m) + abFilterUp, // 31 Filter + (под shift → 60m) + abBandDown, // 32 Band - (под shift → 40m) + abModeDown, // 33 Mode - (под shift → 30m) + abFilterDown, // 34 Filter - (под shift → 20m) + abVFOAtoB, // 35 A>B (под shift → 17m) + abVFOBtoA, // 36 B>A (под shift → 15m) + abVFOSplit, // 37 Split (под shift → 12m) + abNone, // 38 (под shift → 10m) + abNone, // 39 (под shift → 6m) + abNone, // 40 (под shift → GEN, нет в ewsdr) + abNone, // 41 RIT/XIT toggle — нет RIT/XIT + abVFOSwap, // 42 Toggle A/B + abVFOLock, // 43 VFO Lock + abCTUN, // 44 CTUNE + abStartStop, // 45 SDR ON (start/stop) + abMOX, // 46 MOX + abTune); // 47 TUN // Раскладка энкодеров: индекс = номер энкодера (0-based), значение = параметр. + // Нумерация = «Encoder Number» Thetis − 1 (дефолт MakeNewAndromedaDataset). + // Энкодер #1 в Thetis — AGC Level (слайдер RF), поэтому aeAGCLevel. + // #4/#5 на панели — Filter High/Low (независимые края), но в ewsdr фильтр + // только симметричный (SetFilterBW): #4 крутит ширину, #5 пока не задействован. 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); + aeAFGain, // 0 RX1 AF Gain + aeAGCLevel, // 1 RX1 AGC Level (Thetis «RF») + aeNone, // 2 RX2 AF — нет второго приёмника + aeNone, // 3 RX2 AGC + aeFilterWidth, // 4 Filter High → ширина фильтра (краёв нет) + aeNone, // 5 Filter Low — не реализован + aeNone, // 6 Diversity gain + aeNone, // 7 Diversity phase + aeNone, // 8 RIT + aeNone, // 9 XIT + aeMulti, // 10 Multifunction + aeDrive, // 11 TX Drive + aeNone, aeNone, aeNone, aeNone, aeNone, aeNone, aeNone, aeNone); // Номер LED (ZZZI nn) для каждого индикатора; 0 = панель этот индикатор не имеет. // 1..11 — дефолт Thetis; 12..17 — функции ewsdr на свободных слотах. diff --git a/AndromedaPanel.pas b/AndromedaPanel.pas index 17fd3e4..f90382c 100644 --- a/AndromedaPanel.pas +++ b/AndromedaPanel.pas @@ -39,16 +39,20 @@ type 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); @@ -126,15 +130,49 @@ begin 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 @@ -151,6 +189,10 @@ begin 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); @@ -191,6 +233,8 @@ begin 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); @@ -206,6 +250,7 @@ begin 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); diff --git a/RadioController.pas b/RadioController.pas index 094993e..5729ef5 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -60,7 +60,7 @@ type // Поле, которое изменилось — для гранулярных уведомлений наружу. TRadioField = ( - rfVfoA, rfVfoB, rfActiveVfo, + rfVfoA, rfVfoB, rfActiveVfo, rfVfoLock, rfMode, rfFilter, rfFilterBW, rfAGCMode, rfAGCTop, rfRxGain, // Pluto: hw-gain (auto/manual dB) @@ -217,6 +217,7 @@ type FTransmitting: Boolean; FTuning: Boolean; FSplitTxB: Boolean; + FVfoLock: Boolean; // блокировка перестройки VFO (Andromeda Lock) FDisplayDuplex: Boolean; // QO-100 full-duplex self-monitor: True — RX-аудио глушится на TX (по умолч.), // False — слышим свой downlink со спутника при передаче. Кнопка RX MUTE. @@ -515,6 +516,7 @@ type procedure SetMOX(On_: Boolean); procedure SetTune(On_: Boolean); // TUN procedure SetSplit(TxOnB: Boolean); // split: TX на VFO-B (иначе VFO-A) + procedure SetVfoLock(On_: Boolean); // блокировка перестройки VFO // FM procedure SetFMStep(Idx: Integer); @@ -1899,6 +1901,7 @@ end; procedure TRadioController.TuneActiveBy(DeltaHz: Double); begin + if FVfoLock then Exit; // VFO заблокирован — перестройку игнорируем if FActiveVfo = 0 then SetVfoA(FVfoA + DeltaHz) else SetVfoB(FVfoB + DeltaHz); end; @@ -2712,6 +2715,15 @@ begin Changed(rfActiveVfo); end; +procedure TRadioController.SetVfoLock(On_: Boolean); +// VFO Lock: пока включён, относительная перестройка (TuneActiveBy — энкодер VFO +// панели, CAT UP/DN) игнорируется. Подсветка — индикатор aiVFOLock (#11). +begin + if FVfoLock = On_ then Exit; + FVfoLock := On_; + Changed(rfVfoLock); +end; + procedure TRadioController.SetFMRpt(Dir: Integer); begin FFMRptDir := Dir; diff --git a/doc/ANDROMEDA.md b/doc/ANDROMEDA.md index f18fa0d..5e75cdc 100644 --- a/doc/ANDROMEDA.md +++ b/doc/ANDROMEDA.md @@ -84,46 +84,66 @@ Andromeda — это **не отдельный набор команд**, а д ## 3. Раскладка по умолчанию (AndromedaMap.pas) -Раскладка **зашита в коде** (первая версия). Правится в `AndromedaMap.pas`. +Раскладка **зашита в коде** и зеркалит дефолтную таблицу Thetis +(`MakeNewAndromedaDataset`): наш 0-based индекс = «Pushbutton/Encoder Number» +Thetis − 1. Номера **проверены на реальной панели** (см. +`andromeda_test/AndDecode.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 | +| 0 | RX1 Mute | 35 | A>B (под shift → 17m) | +| 28 | **Shift** (band-keypad) | 36 | B>A (под shift → 15m) | +| 29 | Band Up (под shift → 160m) | 37 | Split (под shift → 12m) | +| 30 | Mode Up (под shift → 80m) | 38 | *(под shift → 10m)* | +| 31 | Filter Up (под shift → 60m) | 39 | *(под shift → 6m)* | +| 32 | Band Down (под shift → 40m) | 42 | VFO Swap (toggle A/B) | +| 33 | Mode Down (под shift → 30m) | 43 | **VFO Lock** | +| 34 | Filter Down (под shift → 20m) | 44 | CTUN | +| | | 45 | SDR ON (start/stop) | +| | | 46 | MOX | +| | | 47 | TUN | + +Не назначены (нет тракта в ewsdr): 2 (RX2 Mute), 40 (band GEN), 41 (RIT/XIT). +NB/NR/ANF/SNB/CTCSS/Squelch у реальной панели на кнопках не выведены +(в Thetis — на тачскрине), действия в enum сохранены для кастомной раскладки. + +### Shift / band-keypad + +Кнопка #28 — модификатор-защёлка (как `eBBShift` в Thetis). Пока активен +(LED `aiShift` #6), кнопки band-группы #29..39 выбирают диапазон напрямую +(`SetBand` 0..10: 160m…6m); #40 = GEN — в ewsdr отсутствует. Снимается +повторным Shift либо автоматически после выбора диапазона (non-sticky). +Логика — в `TAndromedaPanel.HandleButton`. ### Энкодеры (`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`) | +| 1 | AGC-T (в Thetis — слайдер «RF») | ±1 dB | +| 4 | Filter High → ширина фильтра | ±50 Гц (`AND_BW_STEP_HZ`) | +| 11 | Drive (мощность) | ±1 % | | VFO | энкодер VFO (`ZZZU/ZZZD`) | ±10 Гц/шаг (`AND_VFO_STEP_HZ`) | +> #4/#5 на панели — независимые края фильтра (Filter High/Low). В ewsdr фильтр +> симметричный (`SetFilterBW`), поэтому #4 крутит ширину, #5 пока не задействован. +> Энкодеры RX2 AF/AGC (#2/#3), diversity (#6/#7), RIT/XIT (#8/#9) — нет тракта. + ### Индикаторы (`ZZZI`, номера зеркалят дефолт Thetis) | LED # | Индикатор | LED # | Индикатор | |---|---|---|---| | 1 | MOX | 12 | Split | | 2 | ATU Ready *(нет в ewsdr)* | 13 | NB | | 3 | Tune | 14 | NR | -| 6 | Shift *(нет)* | 15 | SNB | +| 6 | Shift | 15 | SNB | | 7 | CTUN | 16 | ANF | | 8/9 | RIT/XIT *(нет)* | 17 | Squelch | | 10 | VFO A/B | | | -| 11 | VFO Lock *(нет)* | | | +| 11 | VFO Lock | | | -Драйвятся реально: **MOX, Tune, CTUN, VFO A/B, Split, NB, NR, SNB, ANF, Squelch**. -Остальные слоты дефолтной таблицы Thetis (ATU/Shift/RIT/XIT/VFOLock) в ewsdr -функций не имеют и не зажигаются. +Драйвятся реально: **MOX, Tune, CTUN, VFO A/B, VFO Lock, Shift, Split, NB, NR, +SNB, ANF, Squelch**. Остальные слоты дефолтной таблицы Thetis (ATU/RIT/XIT) +в ewsdr функций не имеют и не зажигаются. --- @@ -145,16 +165,21 @@ Andromeda — это **не отдельный набор команд**, а д ## 5. Что сделано / что осталось -### Сделано (первая версия) +### Сделано - Транспорт: **serial** (USB CAT). - Вход: `ZZZD/ZZZU` (VFO), `ZZZE` (энкодеры), `ZZZP` (кнопки), `ZZZS` (рукопожатие). - Выход: **`ZZZI`** — LED-индикаторы (push от `OnStateChanged`, дедуп повторов). -- Раскладка — зашитый дефолт. +- Раскладка — зашитый дефолт, выверена по Thetis и реальной панели. +- **Shift / band-keypad** — прямой выбор диапазона (#29..39 → `SetBand`), LED `aiShift`. +- **VFO Lock** — блокировка перестройки (`SetVfoLock`, гейт в `TuneActiveBy`), LED `aiVFOLock`. +- A>B / B>A / Toggle A/B, SDR ON (start/stop) — на штатных кнопках панели. - UI-чекбокс на каждый serial-порт + персист. ### Осталось / возможные улучшения | Задача | Заметки | |---|---| +| **Filter High/Low (`ZZZE` #4/#5)** | независимые края фильтра; сейчас #4 = ширина, #5 не задействован. Нужны `FFilterLow/High` + переработка `ApplyModeFilter` и passband дисплея (sideband-aware, как в Thetis) | +| **Sticky shift** | сейчас shift всегда non-sticky (снимается band-нажатием); в Thetis есть `AndromedaStickyShift` | | **`ZZMF`** — текст на LCD мультифункц. энкодера | кодировка: 15 ASCII-символов парами цифр (код−32) | | **TCP-транспорт (G2 V2 panel)** | сейчас только serial; нужен режим в `CATTcp` | | **Настраиваемая раскладка** | сейчас зашита; вынести в конфиг/UI (как `AndromedaEditForm` в Thetis) |