Files
ewsdr/doc/SLICES_PLAN.md
T
ew8bakandClaude Opus 4.8 b2fa2756cf docs(slices): draft Stage 3 notes (HW DDC design + UI concept)
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>
2026-07-02 14:36:31 +03:00

282 lines
23 KiB
Markdown
Raw 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.
# Мультислайсы (несколько приёмных фильтров, как во 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). Ортогонально «внутри/вне полосы».
**Принятые решения (предварительно):**
- Лимит: **до 46 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)