mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
Record grounded data-path analysis, per-DDC/RX2 deltas, the soft-vs-hard WDSP channel split, preliminary decisions (4-6 hard DDCs, static first), and the UI concept (edge markers, slices rail, ADC badge). Marked as a work-in-progress draft. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
282 lines
23 KiB
Markdown
282 lines
23 KiB
Markdown
# Мультислайсы (несколько приёмных фильтров, как во FlexRadio)
|
||
|
||
Статус: **Этапы 1 + 2 РЕАЛИЗОВАНЫ и ЗАКОММИЧЕНЫ** (ветка `feature/multislice`,
|
||
коммит `1527b0f`; проверено на эфире) + UI-полировка флагов (`30a6616`,
|
||
`3049f91`, `a3ac459`). Этап 3 (аппаратные DDC для слайса вне захваченной
|
||
полосы) — не начат. Ниже: что вошло по факту + дизайн/остаток.
|
||
|
||
## Что реализовано (Этапы 1 + 2)
|
||
|
||
- **WDSPEngine** — софтовый fan-out: `TSliceChan` + `FSlices[0..MAX_SLICES-1]`
|
||
(`MAX_SLICES=6`), свои WDSP-каналы (`SLICE_CHAN_BASE=2`), `ProcessSlices` в
|
||
DSP-потоке (свежая копия IQ на канал, свой NCO/mode/filter/AGC/NR/NB/SNB/ANF +
|
||
S-метр → `OnSliceAudio`), `FSliceLock` сериализует add/remove/set vs DSP,
|
||
переоткрытие после смены sample rate.
|
||
- **RadioController** — `TCtrlSlice` (абс. `TargetHz`, буква, свой `TAudioOutput`/
|
||
звуковуха), `Add/Remove/SetSlice*`, `RefreshSliceShifts` держит слайсы на
|
||
`TargetHz` при смене центра, гейт `SliceFitsCapture` (вне полосы = Этап 3).
|
||
Буквы доп. слайсов начинаются с `'B'` (A = главный приёмник).
|
||
- **UI** — по флагу `TVfoOverlay` на слайс (буква-бейдж, «×», fly-out MODE/DSP/AUD,
|
||
vol/mute/split/TX, S-метр), маркеры полосы фильтра слайса в **обоих** рендерерах
|
||
(GL `DrawSliceFilterMarkers` + CPU), drag-ретюн несущей, колесо над флагом.
|
||
Ctrl+ЛКМ по спектру = добавить слайс. Флаг главного (A) виден когда скрыта
|
||
левая панель ЛИБО есть слайсы B+ (синхронизируется с правками с левой панели).
|
||
- **UI-полировка флагов** (`30a6616`, `3049f91`, `a3ac459`):
|
||
- `MainForm.LayoutFlags` — единый packing всех флагов (A+B+) в один ряд: каждый
|
||
рядом со своей полосой фильтра, но флаги НЕ наезжают (сосед слева толкает
|
||
правого; обратный поджим при выходе за правый край). `PositionVfoOverlay`/
|
||
`PositionSliceFlags` — тонкие обёртки над ним.
|
||
- Идентификация уехавшего флага — буквой слайса по центру его полосы фильтра
|
||
сверху (`DrawBandLetter` CPU + `DrawBandLetterGL` GL с кэшем текстур букв);
|
||
у главного A буква только когда есть слайсы.
|
||
- Палитра слайсов `VfoOverlay.SliceColor(L)` (A зелёный … F фиолетовый, не
|
||
красный — не путать с TX): один цвет для бейджа-буквы во флаге и для буквы
|
||
на полосе.
|
||
- Кнопки флага — общий `FlatButton.PaintFlatButton` (плоские, как левая
|
||
панель; без «стеклянного» градиента), центровка бейджа через `TextRect`.
|
||
- Флаг полностью тематизирован: структурные `CLR_*` выводятся из `TAppTheme`
|
||
(`ApplyThemeColors`), тема раздаётся из `ApplyDarkTheme` + `WireOverlayEvents`.
|
||
Статус-акценты (TX/SPLIT) фиксированы намеренно.
|
||
- **Перф** — S-метр развязан с кэшем «хрома» флага (`FMeterDirty` + meter-only
|
||
`RefreshMeter` через `EnsureRendered`); `InvalidateVfoOverlay` не дёргает
|
||
глобальный перезалив оверлеев на тик метра.
|
||
|
||
Остаток/риски (не сделано): персист слайсов в `Settings` (список + устройство);
|
||
per-slice AGC-Top (сейчас общий с главным); полноценный мультислайс-TX (пока TX
|
||
только главный); Этап 3 (HW DDC). Исходный план/дизайн — ниже без изменений.
|
||
|
||
## Предыстория (UI-каркас до мультислайса — исторично)
|
||
|
||
> Ниже — состояние ДО реализации Этапа 2 (одиночный флаг, захардкоженная `'A'`).
|
||
> Актуальное состояние см. в «Что реализовано» выше. Оставлено как контекст.
|
||
|
||
`VfoOverlay.pas` переделан в **«флаг слайса»** в стиле SmartSDR (6 коммитов
|
||
`4a92256..a04c592`, уже в `main`). Это ровно UI-управление одним слайсом из
|
||
Этапа 2, только пока в единственном экземпляре:
|
||
|
||
- Флаг-накладка поверх спектра с буквой слайса (`FSlice`, сейчас захардкожено `'A'`),
|
||
fly-out пикерами **MODE / DSP / AUD**, слайдером громкости, mute, SPLIT,
|
||
кликабельным **TX-бейджем** (выбор TX-слайса) и S-метром.
|
||
- **Per-flag выбор аудио-устройства** уже есть: `SetAudioDevices` + `OnAudioDevice`
|
||
(`VfoOverlay.pas:31,125,136`) — прокручиваемый список в AUD-fly-out. Это готовый
|
||
UI под «своё звуковое устройство на слайс» из абстракции `TSlice.DeviceIndex`.
|
||
- События наружу: `OnSelect/OnDSPChange/OnAGCChange/OnVolume/OnMute/OnAudioDevice/`
|
||
`OnSplit/OnTxSelect/OnInvalidate` (`VfoOverlay.pas:131-139`).
|
||
- Автопозиционирование: флаг флипается влево, если не влезает справа (`6ad5f29`).
|
||
|
||
Привязка в приложении **пока одиночная**: один `FVfoOverlay` в `MainForm`
|
||
(`MainForm.pas:125`), созданный/подключённый на `MainForm.pas:962-972`, привязан к
|
||
VFO A (`UpdateVfo(FController.FVfoA)`, `MainForm.pas:3265`); хендлеры
|
||
`OnVfoOverlay*` шлют изменения в главный тракт. `SpectrumView` держит один
|
||
`FVfoOverlay` и рисует его в `DrawOverlay` (`SpectrumView.pas:72,220,1010`).
|
||
|
||
Чтобы превратить это в мультислайс — нужно из одиночного флага сделать **коллекцию
|
||
флагов (по одному на слайс)** и развести их события на конкретный `SliceId` вместо
|
||
глобального VFO A. Детали — в обновлённом Этапе 2.
|
||
|
||
## Цель
|
||
|
||
Несколько независимых приёмных «слайсов» на одном ADC/одном IQ-потоке:
|
||
у каждого слайса своя частота, режим, полоса, AGC и **своё звуковое
|
||
устройство** (как DAX/наушники/динамик во Flex). Панадаптер (спектр+водопад)
|
||
остаётся один, слайсы рисуются маркерами поверх.
|
||
|
||
## Как это устроено у Flex и что из этого берём
|
||
|
||
Слайс во Flex — это **не «ещё один фильтр» на одном приёмнике**, а полноценный
|
||
независимый узкополосный приёмник (свой DDC в FPGA + демодулятор + фильтр + AGC +
|
||
аудио). Общий у слайсов — только панадаптер (широкий FFT-захват).
|
||
|
||
Один WDSP-канал = один демодулятор = один фильтр = один аудиопоток. Значит для
|
||
«разный звук/режим/полоса на каждый слайс» нужен **отдельный WDSP-канал на слайс**.
|
||
Способ получить узкополосный IQ для канала различается по железу — см. ниже.
|
||
|
||
## Ключевое различие бэкендов
|
||
|
||
```
|
||
openHPSDR (Protocol 2, V4.3, MAX_DDCS=80):
|
||
ADC0 ─┬─ DDC (FPGA-тюнер) → IQ-поток порт 1035+n → WDSP-канал → фильтр/AGC → AudioOut[dev]
|
||
├─ DDC → ...
|
||
└─ DDC → ...
|
||
• Аппаратные DDC: слайс тюнится КУДА УГОДНО в полосе ADC (весь КВ), дёшево по CPU.
|
||
• На Angelia/Orion/Saturn DDC0/DDC1 заняты PureSignal → слайсы с DDC2+ (в коде DDCBase=2).
|
||
• RX2 = ВТОРОЙ ADC (ADC1), отдельная антенна/diversity — ОРТОГОНАЛЬНО слайсам, не нужно.
|
||
|
||
Pluto (AD9363, libiio):
|
||
один RX-тракт: один LO, один rf_bandwidth, один IQ-поток (притворяется одним DDC,
|
||
пушится как TDDCIQPacket в тот же конвейер).
|
||
• Аппаратных DDC НЕТ. Слайсы = СОФТОВЫЙ fan-out одного IQ-блока на N WDSP-каналов,
|
||
каждый со своим SetRXAShiftFreq в пределах захваченной полосы вокруг LO.
|
||
• Для QO-100 идеально: весь транспондер (~500 кГц) в захвате, LO залочен на маяк.
|
||
```
|
||
|
||
## Единая абстракция
|
||
|
||
```pascal
|
||
TSlice = record
|
||
Id: Integer;
|
||
TargetHz: Double; // абсолютная частота слайса
|
||
Mode: TRxMode; // USB/LSB/CW/FM/...
|
||
FilterLo,
|
||
FilterHi: Integer;
|
||
AGCMode: TWDSPAGCMode;
|
||
Volume: Double;
|
||
WDSPChannel: Integer; // канал WDSP (0..N); 0=главный, 1=TX заняты
|
||
Source: (srcSoftShift, srcHardDDC); // как получаем узкий IQ
|
||
DDCIndex: Integer; // если srcHardDDC — какой аппаратный DDC
|
||
ShiftHz: Double; // если srcSoftShift — сдвиг = TargetHz - CapturedCenterHz
|
||
AudioOut: TAudioOutput; // свой инстанс со своим DeviceIndex
|
||
DeviceIndex: Integer; // выбранная звуковуха (или -1 = default)
|
||
Enabled: Boolean;
|
||
end;
|
||
```
|
||
|
||
Правило маршрутизации по частоте (одно на оба бэкенда):
|
||
- слайс **внутри** текущего захваченного span → `srcSoftShift` (Pluto и HPSDR);
|
||
- слайс **вне** захваченного span → `srcHardDDC` (только HPSDR; Pluto так не умеет).
|
||
|
||
## Порядок работ
|
||
|
||
### Этап 1 — Софтовый fan-out в WDSPEngine (backend-agnostic) ✅ РЕАЛИЗОВАН
|
||
|
||
Даёт слайсы сразу на Pluto/QO-100 (единственный режим) и на HPSDR для слайсов
|
||
внутри видимой полосы. Pluto уже пушит IQ через тот же DDC-конвейер, поэтому
|
||
fan-out живёт в движке и не зависит от бэкенда.
|
||
|
||
- `WDSPEngine.pas`: сейчас `RXA_CHAN=0` один канал, один `FRXOut`, один аудиовыход
|
||
(`WDSPEngine.pas:96`, alloc буферов ~`:159`, OpenChannel ~`:1081`).
|
||
- Добавить массив слайс-каналов: для каждого активного слайса `OpenChannel(chN, ...)`
|
||
с той же геометрией, что RXA_CHAN, + `SetRXAShiftRun/SetRXAShiftFreq(chN, ShiftHz)`
|
||
(механизм уже используется: `WDSPEngine.pas:1099-1100`), `SetRXAMode`,
|
||
`RXASetPassband`, AGC, `SetRXAPanelGain1`.
|
||
- В DSP-потоке после накопления IQ-блока (`FRXAccI/Q`, `FRXAccPos`) прогонять
|
||
`fexchange0` по КАЖДОМУ активному каналу из ОДНОГО и того же входного буфера
|
||
(входной буфер не портить in-place между каналами — держать копию, как `FSpecBuf`).
|
||
- Выход каждого канала → свой callback `OnSliceAudio(SliceId, L, R, Count)`.
|
||
- `AudioOutput.pas`: уже готов к мульти-устройству — `DeviceIndex`, `EnumOutputDevices`,
|
||
`FindDeviceByName` (`AudioOutput.pas:137-151`). Создавать **отдельный `TAudioOutput`
|
||
на слайс**, каждый со своим `DeviceIndex`.
|
||
- `RadioController.pas`: `TSlice[]` (add/remove/retune/setmode/setdevice), роутинг
|
||
`OnSliceAudio` → нужный `TAudioOutput`.
|
||
|
||
Проверка Этапа 1: Pluto/QO-100 — 2 слайса (VFO + маяк-монитор), звук в две разные
|
||
звуковухи, независимые режимы/полосы. Сборка: `lazbuild --ws=qt6 ewsdr.lpr`.
|
||
|
||
### Этап 2 — UI слайсов на водопаде + выбор устройства ✅ РЕАЛИЗОВАН (кроме персиста)
|
||
|
||
Каркас флага слайса уже сделан (см. «Что уже готово»): один `TVfoOverlay` со всеми
|
||
пикерами и per-flag выбором устройства. Остаётся размножить его на N слайсов и
|
||
привязать к позиции по частоте. Выпадашка устройства и dropdown-паттерн **уже есть в
|
||
самом флаге** (AUD fly-out) — отдельный `DeviceForm` не нужен.
|
||
|
||
- **Коллекция флагов вместо одного.** Из `MainForm.FVfoOverlay` (одиночный,
|
||
`MainForm.pas:125`) сделать `array of TVfoOverlay` / список, по флагу на слайс;
|
||
`SpectrumView.FVfoOverlay` (`SpectrumView.pas:72`) — тоже коллекция, рисовать все в
|
||
`DrawOverlay` (`SpectrumView.pas:1010`). Разместить: `SetSlice(Id, Letter)` в оверлей,
|
||
события `OnVfoOverlay*` должны нести `SliceId` (сейчас бьют по VFO A напрямую —
|
||
завести маппинг оверлей→слайс, роутить в нужный `TSlice`/WDSP-канал из Этапа 1).
|
||
- **Позиционирование по частоте.** `PositionVfoOverlay` (`MainForm.pas:518`) сейчас
|
||
ставит один флаг; расширить на маппинг `TargetHz → X` через `GetViewWindow`/
|
||
`SyncSpecViewFreq` (зум-работа). Слайс за краем видимой полосы — маркер у края
|
||
(флип влево/вправо уже есть, `6ad5f29`).
|
||
- **ЛКМ по водопаду** = поставить/двинуть активный слайс; выделение активного
|
||
(подсветка буквы/рамки флага). Хит-тест мыши флага уже разведён
|
||
(`HandleMouseDown/Move/Up`, `MainForm.pas:4424`) — добавить «клик мимо флага по
|
||
спектру = ретюн активного слайса».
|
||
- **TX-select** уже кликабелен (`OnTxSelect`, `08c71c4`) — при мультислайсе выбирать,
|
||
какой слайс идёт в TX; снимать флаг с остальных.
|
||
- Персист: список слайсов в `Settings.pas` (per-band или глобально — решить), включая
|
||
`DeviceIndex`/имя устройства каждого флага.
|
||
|
||
### Этап 3 — Аппаратные DDC для openHPSDR (слайс куда угодно по КВ) ⏳ НЕ НАЧАТ
|
||
|
||
Апгрейд поверх общего каркаса; нужен только чтобы ставить слайс ВНЕ захваченной
|
||
полосы. Плита ~на 70% есть:
|
||
- `ConfigureDDCs(NumDDCs, ...)` уже включает N DDC с per-DDC конфигом/ADC-source
|
||
(`HPSDRNetwork.pas:220`, `DDCConfig[ddc*6+0]=ADCSource`).
|
||
- Приём разведён по DDC: порты `1035+n` → `HandleDDCIQ(Buf,Len,DDCIdx)`
|
||
(`HPSDRNetwork.pas:392`).
|
||
- High Priority уже держит частоту на КАЖДЫЙ DDC (bytes 9-12=DDC0, 13-16=DDC1, …)
|
||
`HPSDRProtocol.pas:271`.
|
||
|
||
Чего не хватает:
|
||
1. Per-DDC частота: сейчас `SetRunAndFreq(...; DDC0FreqHz; ...)` знает только DDC0
|
||
(`HPSDRNetwork.pas:224`) — писать частотное слово каждого DDC в High Priority.
|
||
2. Роутинг `HandleDDCIQ(DDCIdx)` → соответствующий WDSP-канал слайса.
|
||
3. Учёт DDCBase=2 (PureSignal) и Ethernet-бюджета (реально 4–8 слайсов, не 80).
|
||
|
||
`feature/rx2-support` — это про ВТОРОЙ ADC, не про слайсы; но её каркас
|
||
(доп. WDSP-канал + доп. аудиовыход + роутинг по индексу) частично переиспользуем.
|
||
|
||
### Этап 3 — рабочие заметки (черновик, обсуждение НЕ закончено)
|
||
|
||
Разобрали фактический data-path и набросали дизайн; вернёмся позже.
|
||
|
||
**Что есть сейчас (по коду):**
|
||
- Один аппаратный DDC: `ConfigureDDCs(1, rate, ADCSource=0)` (`RadioController.pas:3314`),
|
||
IQ на порт `1035+n` → `HandleDDCIQ(DDCIdx)` → `OnDDCIQ(DDCIndex)`. В WDSP подаётся
|
||
**только `FActiveDDC`** (`RadioController.pas:843`), остальные DDC отбрасываются.
|
||
- Слайсы = софт fan-out этого одного IQ: `SetSliceShift(Id, TargetHz-FCenterFreq)`
|
||
(`RefreshSliceShifts:1063`). Гейт `SliceFitsCapture` ±47.5% rate вокруг центра
|
||
(`:1081`); вне полосы `AddSlice` отказывает (`:1097`).
|
||
- HP-фрейм шлёт частотное слово **только DDC0**: `Buf[9 + DDCBase*4]`
|
||
(`SendFullHP:1567`); слова остальных DDC = нули.
|
||
- `ConfigureDDCs` умеет N DDC (per-DDC enable), но пишет **один** rate/ADCSource на все;
|
||
`DDCBase=2` на Orion/Angelia/Saturn (DDC0/1=PureSignal), `NumADCs=2` на 2-АЦП платах.
|
||
|
||
**Что нужно (дельты по точкам):**
|
||
1. Per-DDC частота в HP: `SendFullHP` — цикл `Buf[9+ddc*4]=FreqToPhaseWord(FDDCFreq[ddc])`;
|
||
контроллер держит `FDDCFreq[]`, обновляет при ретюне слайса.
|
||
2. Per-DDC rate + ADCSource в `ConfigureDDCs` (принимать массив): главный DDC широкий
|
||
(панорама), слайс-DDC узкие ~48–96 кГц (дёшево по линку). Протокол это позволяет.
|
||
3. Аллокатор DDC: пул hw-индексов от `DDCBase+1`, захват/освобождение, кап (см. решения).
|
||
4. **Ключевое — два типа слайс-канала в WDSPEngine:**
|
||
- *soft* (Этап 1): вход = копия главного IQ, rate главного, NCO shift = `Target-Center`,
|
||
общий аккумулятор.
|
||
- *hard* (Этап 3): вход = **свой** DDC-пакет, rate его DDC, NCO shift = 0, **свой**
|
||
аккумулятор `FSliceAccI/Q`. Нужен `PushSlicePacket(chan, ...)` + роутинг
|
||
`OnDDCIQ(DDCIndex)→WDSP-канал привязанного слайса`.
|
||
5. RX2 (второй ADC): `TSlice.ADCSource (0/1)` → слайс-DDC на нужный ADC. Даёт двух-антенные
|
||
слайсы (главный RX на ANT1, слайс на ANT2). Ортогонально «внутри/вне полосы».
|
||
|
||
**Принятые решения (предварительно):**
|
||
- Лимит: **до 4–6 hard-DDC слайсов**, со строгим контролем суммарного rate (бюджет Ethernet).
|
||
- **Сначала статика**: источник (soft/hard) выбирается при добавлении/явном ретюне;
|
||
авто-promotion/demotion (soft↔hard при смене центра/зума) — отдельным этапом позже.
|
||
- RX2/ADCSource — не решено (нужно знать, как заведены антенны у пользователя).
|
||
|
||
**UI-концепция (черновик, не финал — выбор представления off-span НЕ сделан):**
|
||
Принцип: **цвет+буква слайса = его личность во всех местах** (флаг, полоса, edge-маркер,
|
||
чип рейла, ADC-бейдж) — переиспользуем палитру `SliceColor`, тему, `PaintFlatButton`.
|
||
- Слайс в полосе — как сейчас (флаг сбоку + буква на полосе, packing).
|
||
- Слайс **вне** видимой панорамы (hard) — приклеенный к краю спектра edge-маркер:
|
||
цветная буква + стрелка (◄/►) + абсолютная частота. Клик = выбрать, двойной = перевести
|
||
главную панораму на него. Почти бесплатно (флип/клэмп к краю уже есть).
|
||
- **Slices-рейл** — тонкая полоса-менеджер (под панорамой или в правой панели): чип на слайс
|
||
(цветная точка+буква · частота · режим · мини-S-метр · ADC-бейдж), сортировка по частоте,
|
||
`[+]` = добавить с вводом частоты (для off-span, где Ctrl-клик не поможет). Единственное
|
||
место, где off-span слайсы всегда видны/управляемы.
|
||
- **ADC/антенна** — бейдж `A1`/`A2` в шапке флага и в чипе (только на 2-АЦП платах).
|
||
- **soft/hard** — тонкий признак (плотная vs полая точка, или значок `⧉`); не кричать.
|
||
- Позже «вау»: мини-панадаптер (свой спектр/водопад) на каждый hard-слайс в чипе рейла.
|
||
|
||
Развилка для следующего захода: как показывать off-span слайсы —
|
||
(a) edge-маркеры + рейл [рекомендация], (b) только edge-маркеры, (c) рейл с мини-панадаптерами.
|
||
|
||
## Оговорки / риски
|
||
|
||
- CPU софтовых слайсов ~линейно от их числа: каждый WDSP-канал жуёт полный rate и
|
||
децимирует сам (аппаратный DDC дешевле — децимирует железо). Для QO-100 span мал,
|
||
несколько слайсов — норма.
|
||
- Не портить входной IQ-буфер между каналами (fexchange0 пишет in-place).
|
||
- Pluto: слайсы только в пределах захваченной полосы; выход за край = ретюн LO
|
||
(двигает всё, при QO-100 LO залочен на маяк — не трогать).
|
||
- Пороговая аккуратность: старт/стоп WDSP-каналов на лету (см. заметку про задержки
|
||
RXA стоп/старт в `WDSPEngine.pas:~230`).
|
||
|
||
## Связанные заметки памяти
|
||
|
||
- project_pluto_integration, project_qo100_zoom_panel, project_qo100_decoder_lock
|
||
- project_spectrum_zoom (GetViewWindow / оверлеи / маппинг мыши)
|
||
- project_channels_feature (dropdown/форма-редактор как паттерн UI)
|