Files
ewsdr/AndromedaMap.pas
ew8bakandClaude Opus 5 25521a2d10 feat(filter): произвольные кромки фильтра — таблица по режиму, редактор, drag, TX-полоска
Фильтр главного приёмника перестал быть одним числом. Теперь это таблица
(Name, Low, High) на режим: 10 пресетов + VAR1/VAR2, знаковые кромки (у LSB
своя строка, у USB своя), паритет с Thetis FilterForm. Слайсы независимые
кромки умели и раньше — главный приёмник настраивался только пресетами.

Модель (Settings/RadioController):
- TFilterSlot/TFilterSet/TFilterTable + заводские дефолты из прежних FILT_*_BW
  и FILT_*_NAMES (переехали в Settings — из них строится таблица).
- FFilterLo/FFilterHi = истина, FFilterBW производная (FilterBWFromEdges),
  поэтому все прежние читатели ширины (web, зум, band-cache, CAT) не тронуты.
- ApplyFilterSlot — единственная точка «слот → состояние тракта».
- SetFilterEdges: произвольные кромки уходят в VAR1 с автопереключением,
  пресеты нельзя испортить случайным движением мыши или энкодера.
- Персист: секция "filters" под MAC, пишутся только режимы, отличные от
  заводских; секция режима сносится целиком, иначе возврат слота к
  заводскому оставлял бы старый ключ.
- Диапазон помнит только выбранный слот: кромки живут в таблице, второго
  источника истины у VAR нет.

UI:
- 12 кнопок фильтра (2 ряда по 6), подписи из живой таблицы.
- FilterPopup — поповер редактора по ПКМ на кнопке фильтра, по образцу
  PureSignalPopup (дочерний контрол формы, не top-level: Wayland). Слева
  слоты, справа Name/Low/High/Width, внизу форма полосы на знаковой оси.
- Перетаскивание края полосы на спектре: ±5px, снап 10 Гц, Alt = симметрично
  (не Ctrl — он создаёт слайс). Отказ на передаче и когда полоса на экране
  уже 12px, иначе захват съедал бы клик-тюн.

Отрисовка:
- Полоса главного VFO рисуется по РЕАЛЬНЫМ кромкам приёмника, слайса — по его
  кромкам; вывод из режима и ширины остался фолбэком. Пара→ширина→пара было
  преобразованием с потерями: несимметричный фильтр из слайс-CAT (ZZFL/ZZFH)
  рисовался не там, где звучал.
- На передаче полоса считается по TX-фильтру (TXSignedEdges), а не по
  приёмной ширине — TX-полоса от RX не зависит (модель Thetis).
- CalcFilterBandXFor больше не держит свою копию таблицы знака боковой:
  единственная таблица теперь FilterEdgesFromBW.

Управление:
- CAT ZZFL/ZZFH заработали на основном порту (были заглушками).
- Андромеда: энкодеры #4/#5 — независимые Filter High/Low, как на железе G2
  (было: #4 ширина, #5 не реализован).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:15:33 +03:00

180 lines
7.8 KiB
ObjectPascal
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit AndromedaMap;
{
AndromedaMap.pas — таблицы раскладки передней панели Apache Labs Andromeda / G2.
Только ДАННЫЕ: перечисления действий и дефолтные таблицы
«номер кнопки → действие», «номер энкодера → параметр»,
«индикатор → номер LED (ZZZI)». Без логики и без зависимостей от
контроллера — этим занимается AndromedaPanel.pas.
Соответствие протоколу Thetis:
• действия кнопок/энкодеров — подмножество EButtonBarActions / EEncoderActions;
• номера LED-индикаторов (ZZZI nn) зеркалят дефолтную таблицу Thetis
(MakeNewAndromedaDataset): 1=MOX, 3=Tune, 7=CTUN и т.д. Свободные слоты
12..17 заняты функциями ewsdr (Split/NB/NR/SNB/ANF/Squelch).
}
{$mode objfpc}{$H+}
interface
type
// Действие кнопки панели (подмножество, поддержанное ewsdr).
TAndButton = (
abNone, abMOX, abTune, abBandUp, abBandDown, abModeUp, abModeDown,
abMute, abNB, abNR, abSNB, abANF, abVFOSplit, abVFOSwap, abCTUN,
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 = (
aeNone, aeAFGain, aeAGCLevel, aeDrive, aeSquelch,
aeFilterWidth, aeFilterHigh, aeFilterLow, aeMulti);
// Индикатор (LED) панели. Порядок зеркалит EIndicatorActions Thetis.
TAndIndicator = (
aiNone, aiMOX, aiTune, aiRIT, aiXIT, aiSplit, aiCTune, aiVFOSync,
aiVFOAB, aiVFOLock, aiNB, aiNR, aiSNB, aiANF, aiSquelch,
aiCompander, aiPuresignal, aiDiversity, aiATUReady, aiShift);
const
// Размеры дефолтных таблиц (кнопки/энкодеры нумеруются с 0 после разбора ZZZP/ZZZE).
AND_MAX_BUTTONS = 48;
AND_MAX_ENCODERS = 20;
// Раскладка кнопок: индекс = номер кнопки (0-based), значение = действие.
// Нумерация совпадает с дефолтом Thetis (MakeNewAndromedaDataset): наш 0-based
// индекс = «Pushbutton Number» Thetis 1. Проверено на реальной панели
// (см. andromeda_test/AndDecode.pas). Неназначенные / отсутствующий тракт → abNone.
AND_BUTTON_MAP: array[0..AND_MAX_BUTTONS-1] of TAndButton = (
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: настоящие независимые края (как на железе G2).
// aeFilterWidth оставлен в перечислении для раскладок, где нужен один
// энкодер «ширина» — в дефолтной раскладке не используется.
AND_ENCODER_MAP: array[0..AND_MAX_ENCODERS-1] of TAndEncoder = (
aeAFGain, // 0 RX1 AF Gain
aeAGCLevel, // 1 RX1 AGC Level (Thetis «RF»)
aeNone, // 2 RX2 AF — нет второго приёмника
aeNone, // 3 RX2 AGC
aeFilterHigh, // 4 Filter High — верхний край
aeFilterLow, // 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 на свободных слотах.
AND_INDICATOR_NUM: array[TAndIndicator] of Integer = (
{aiNone} 0,
{aiMOX} 1,
{aiTune} 3,
{aiRIT} 8,
{aiXIT} 9,
{aiSplit} 12,
{aiCTune} 7,
{aiVFOSync} 0,
{aiVFOAB} 10,
{aiVFOLock} 11,
{aiNB} 13,
{aiNR} 14,
{aiSNB} 15,
{aiANF} 16,
{aiSquelch} 17,
{aiCompander} 0,
{aiPuresignal} 0,
{aiDiversity} 0,
{aiATUReady} 2,
{aiShift} 6);
{ Возвращает действие для номера кнопки (0-based); abNone вне диапазона. }
function ButtonAction(BtnIndex: Integer): TAndButton;
{ Возвращает параметр для номера энкодера (0-based); aeNone вне диапазона. }
function EncoderParam(EncIndex: Integer): TAndEncoder;
{ Номер ZZZI для индикатора; 0 если панель его не отображает. }
function IndicatorNum(Ind: TAndIndicator): Integer;
implementation
function ButtonAction(BtnIndex: Integer): TAndButton;
begin
if (BtnIndex >= 0) and (BtnIndex < AND_MAX_BUTTONS) then
Result := AND_BUTTON_MAP[BtnIndex]
else
Result := abNone;
end;
function EncoderParam(EncIndex: Integer): TAndEncoder;
begin
if (EncIndex >= 0) and (EncIndex < AND_MAX_ENCODERS) then
Result := AND_ENCODER_MAP[EncIndex]
else
Result := aeNone;
end;
function IndicatorNum(Ind: TAndIndicator): Integer;
begin
Result := AND_INDICATOR_NUM[Ind];
end;
end.