# Мультислайсы и панадаптеры (как во FlexRadio) Статус: **Этапы 1 + 2 РЕАЛИЗОВАНЫ и ЗАКОММИЧЕНЫ** (ветка `feature/multislice`, коммит `1527b0f`; проверено на эфире) + UI-полировка флагов (`30a6616`, `3049f91`, `a3ac459`). **2026-07-10: Этап 3 ПЕРЕПРОЕКТИРОВАН.** Вместо старой идеи «off-span слайс с edge-маркером» принята Flex-модель: **аппаратный DDC = отдельный панадаптер** (свой спектр+водопад), а слайсы ВСЕГДА живут внутри какого-то панадаптера. Старый черновик (edge-маркеры + slices-рейл) перенесён в «Отвергнутые альтернативы» в конце файла. Реализация Этапа 3 не начата. --- ## Что реализовано (Этапы 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+) в один ряд; идентификация уехавшего флага буквой по центру полосы (`DrawBandLetter` CPU + `DrawBandLetterGL` GL с кэшем текстур букв); палитра `VfoOverlay.SliceColor(L)`; кнопки через `FlatButton.PaintFlatButton`; полная тематизация из `TAppTheme`. - **Перф** — S-метр развязан с кэшем «хрома» флага (`FMeterDirty` + meter-only `RefreshMeter` через `EnsureRendered`); `InvalidateVfoOverlay` не дёргает глобальный перезалив оверлеев на тик метра. Остаток Этапов 1-2 (не сделано): персист слайсов в `Settings` (список + устройство); per-slice AGC-Top (сейчас общий с главным); полноценный мультислайс-TX (пока TX только главный). --- ## Модель FlexRadio (итог разбора 2026-07-10) Во Flex **две независимые сущности**, и это ключ ко всему дизайну: - **Панадаптер (panafall)** — спектр+водопад. Самостоятельный объект: свой широкополосный захват в железе, свой центр/span/зум, свои настройки водопада. - **Слайс** — узкополосный приёмник, ВСЕГДА прикреплён к какому-то панадаптеру и рисуется на нём флагом. Слайс никогда не имеет собственного спектра. Тянешь слайс за край — панорама скроллится за ним. Хочешь слушать другой участок — открываешь **второй панадаптер** (радио выделяет ещё один захват) и создаёшь слайсы в нём. Лимиты: 6400 — 2 пана/2 слайса, 6600 — 4/4, 6700 — 8/8. **Второй приёмник (второй АЦП)** у Flex называется SCU (Spectral Capture Unit): полный тракт «антенный вход → АЦП → захват». 6600/6700 — два SCU, 6400 — один. У панадаптера НЕТ переключателя «RX1/RX2» — есть селектор **антенны** (ANT1/ANT2/RXA/RXB/XVTR), и радио само сажает панадаптер на SCU под выбранную антенну. Слайсы наследуют антенну своего пана. Diversity (когерентное сложение двух SCU) — отдельный режим поверх. **Окна:** все панафоллы тайлятся ВНУТРИ одного главного окна (сплиты с перетаскиваемыми разделителями). Вынос панафолла в отдельное OS-окно штатный SmartSDR НЕ умеет — наш pop-out будет улучшением поверх Flex-модели. --- ## Итоговая модель EWSDR ``` Панадаптер (спектр+водопад; свой захват IQ; свой ADC-источник; свой span/зум/ │ палитра/wf-AGC; свой WDSP-анализатор) └─ Слайсы (флаги внутри панорамы; софт-fan-out IQ ЭТОГО панадаптера; свой WDSP-канал/режим/фильтр/AGC/DSP/звуковуха — уже реализовано) ``` Правила: 1. Слайс всегда принадлежит панадаптеру (`PanId`). Понятия «off-span слайс» больше нет: хочешь слайс вне захвата — создай панадаптер там. 2. Пан 0 (главный) существует всегда; его «слайс A» — текущий главный тракт (WDSP-канал 0). Панадаптеры N>0 живут целиком на слайс-механике Этапа 1 (каналы `SLICE_CHAN_BASE+`), при создании пана автоматически создаётся один слайс в его центре (поведение Flex). 3. ADC-источник — свойство **панадаптера**, не слайса (бейдж `A1`/`A2` в шапке). В отличие от Flex показываем именно АЦП, а не антенну: на HPSDR-платах маппинг «вход→АЦП» жёсткий. Антенны ADC0 (Alex ANT1/2/3) — отдельная ГЛОБАЛЬНАЯ ручка, общая для всех панов этого АЦП (детали в 3.4). 4. Буквы слайсов — глобальный пул A..G через все паны (как во Flex A-H): буква = личность слайса, пан = место жительства. 5. Возможности бэкенда описывает запись capabilities (см. таблицу) — UI по ней прячет/блокирует кнопки, а не хардкодит «if Pluto». Матрица бэкендов: | Бэкенд | Панадаптеров | Незав. частоты панов | АЦП | Примечание | |-------------------------|--------------|----------------------|-----|------------| | HPSDR 1 ADC (Hermes) | до 4 (DDC) | да | 1 | ADC-селектор скрыт | | HPSDR 2 ADC (Angelia/Orion/Saturn) | до 4 (DDC, DDCBase=2) | да | 2 | селектор A1/A2 на пан | | Pluto (обычный) | 1 | — | 1 | как сейчас, ничего не меняется | | Pluto+ (2r2t) | 2 | **нет — общий LO** | 2 | пан RX2 частотно связан с RX1 | ``` openHPSDR (Protocol 2, MAX_DDCS=80): ADC0 ─┬─ DDC2 (широкий) → Пан 0 → анализатор + слайсы A,B… └─ DDC3 (узкий) → Пан 1 → анализатор + слайсы C,D… ADC1 ──── DDC4 → Пан 2 (другая антенна) → … • Каждый DDC тюнится куда угодно, свой rate. DDC0/1 заняты PureSignal на Angelia/Orion/Saturn (DDCBase=2, HPSDRNetwork.pas:1485). Pluto+ (AD9363 2r2t, libiio): RX LO ─┬─ RX1 → псевдо-DDC0 → Пан 0 (один!)└─ RX2 → псевдо-DDC1 → Пан 1 (та же частота/rate, другая антенна) • Оба канала на ОДНОМ LO/rate, фазокогерентны (база под diversity/пеленгацию). • Обычный Pluto: один RX разведён на плате — Пан 1 недоступен. ``` --- ## Этап 3 (переработан): «DDC = панадаптер» Подэтапы упорядочены так, чтобы каждый собирался и работал сам по себе. 3.0 — чистый рефакторинг без новых фич, дальше функциональность наращивается. ### 3.0 — TPanafallPanel: выделить панадаптер из MainForm **Статус: фазы A и B1 СДЕЛАНЫ (коммиты c125e59, cc9919c) и ПРОВЕРЕНЫ пользователем на экране (A — 2026-07-10, B1 — 2026-07-11: создание/закрытие флагов, видимость флага A, мышь/колесо/drag, тик S-метров — работает).** Создан `PanafallPanel.pas`: - Фаза A: панель владеет view (CPU/GL), контролами (спектр/линейка/сплиттер/ водопад/пан-зум/кнопки зума), полной геометрией стека (`Layout`), механикой сплиттера и мышью пан/зум-полосы. MainForm держит алиасы (`FSpecView`, `PbSpectrum`, …) на члены `FPan`; размеры областей — из `FPan.SpectrumWidth/SpectrumHeight/WaterfallHeight`. - Фаза B1: панель владеет коллекцией флагов слайсов (LayoutFlags, диспетчер мыши флагов, Create/DestroySliceFlag, PushSliceFlagState, TickFlags, UpdateMainFlagVisibility). Флаг A создаёт MainForm и отдаёт через `AttachMainFlag`; семантические события флагов проводятся `OnWireFlag`, роутинг по `FPan.CurSliceId`. **Решение по фазе B2 (мышь спектра/водопада, dirty-флаги/тик, SampleRate/ BandPlan-оверлеи): НЕ переносить сейчас.** Семантика мыши (тюн VFO A/B, XVTR-клампы, маяк, CTUN-drag) станет per-pan только в 3.3, когда появится API «ретюн пана» — перенос сейчас потребовал бы хуков (ApplyVfoAHook, BeaconSeedHook…), которые в 3.3 переделались бы заново. Мышь/тик переезжают в панель В СОСТАВЕ 3.3 вместе с per-pan семантикой. Для 3.1/3.2 текущего состояния панели достаточно. **GL-аудит per-instance ПРОЙДЕН**: в `SpectrumViewOpengl`/`WaterfallViewOpengl` все текстуры (`FGridLabelTex`…`FLetterTex`, `FSliceTex`, `FTexture`, `FMarkerTex`) и LUT водопада — поля экземпляра, глобальных/class var GL-ресурсов нет; палитры — read-only const (шарить безопасно). Самый большой и самый важный кусок. Сейчас панадаптер размазан по MainForm: `FSpecView: TSpectrumView` создаётся и прошивается ~20 свойствами напрямую (`MainForm.pas:1018-1035`), канва `PbSpectrum: TControl` (TOpenGLControl, `MainForm.pas:2220`), `FPanZoomBar` (`MainForm.pas:312`), мышиные хендлеры `PbSpectrumMouse*` (`MainForm.pas:439-445`), `LayoutFlags`, тик обновления. Сделать composite `TPanafallPanel` (новый модуль `PanafallPanel.pas`, наследник TCustomControl/TPanel), который владеет: - канвой (`TOpenGLControl` или `TPaintBox` по `USE_OPENGL` — как сейчас); - своим `TSpectrumView`/`TSpectrumViewOpenGL`; - своим `TPanZoomBar` и линейкой; - коллекцией флагов слайсов этого пана (`FVfoOverlay` пана + `FSliceFlags`) и `LayoutFlags` (packing переезжает сюда); - мышиной логикой канвы (тюн/drag/колесо/Ctrl+ЛКМ/drag маркера слайса); - шапкой пана (см. 3.3) — в пане 0 шапка скрыта до появления пана 1. Наружу: события `OnTune(PanId, Hz)`, `OnAddSlice(PanId, Hz)`, `OnSlice*` (существующие слайс-события + PanId), `OnPanClose/OnPanPopOut/OnPanADC`; входные сеттеры состояния (центр/span/mode/wf-настройки) — то, что сейчас раздаётся FSpecView напрямую. MainForm после рефакторинга держит `FPans: array of TPanafallPanel` (пан 0 всегда), общий тик раздаёт кадры по панам, весь остальной код (левая панель, FreqDisplay, контроллер) работает с «активным слайсом» — механика `FActiveFlag/CurSliceId` уже есть, добавляется лишь `ActivePan`. Критерий готовности 3.0: поведение приложения ИДЕНТИЧНО текущему (один пан, все фичи слайсов работают), diff в MainForm — только вынос. Отдельная задача внутри 3.0 — **аудит GL-ресурсов** `SpectrumViewOpengl` на per-instance: каждое окно/канва = свой GL-контекст, текстуры между контекстами НЕ шарятся. Проверить: текстуры водопада, LUT палитры, кэш текстур букв (`DrawBandLetterGL`), текстуры флагов (`FSliceTex`), шейдеры/списки — ничего static/глобального. (CPU-LUT палитры общий — это ок.) ### 3.1 — Бэкенд HPSDR: per-DDC частота/rate/ADC + роутинг Плита готова ~на 70 %, дельты точечные: 1. **Per-DDC частота в High Priority.** `SendFullHP` (`HPSDRNetwork.pas:1552`) пишет частотное слово только активного DDC (`Buf[9 + DDCBase*4]`). Сделать цикл по активным DDC: `Buf[9+ddc*4] := FreqToPhaseWord(FDDCFreq[ddc])`. Контроллер держит `FDDCFreq: array[0..MAX_DDCS-1] of Double`, обновляет при ретюне пана. 2. **Per-DDC rate/ADC/enable в `ConfigureDDCs`** (`HPSDRNetwork.pas:1460`): сейчас один rate/ADCSource на все N. Принимать массив записей `(Enabled, RateKHz, ADCSource)` — протокол это позволяет (`DDCConfig[ddc*6+0]=ADCSource`, rate там же per-DDC). 3. **Роутинг IQ по панам.** `RadioController.OnDDCIQ` (`RadioController.pas:828`) сейчас гейтит `DDCIndex = FActiveDDC` (`:875`), остальное выбрасывает. Заменить на маппинг `DDCIndex → PanId`: пакет пана 0 — в существующий путь, пакеты панов N>0 — в `WDSPEngine.PushPanIQ(PanId, Data)` (см. 3.2). 4. **Аллокатор DDC**: пул hw-индексов от `DDCBase+1` (DDCBase=2 на PureSignal-платах, `HPSDRNetwork.pas:1485`), захват при создании пана, освобождение при закрытии, слот-стабильность как у слайсов. 5. **Rate слайс-панов**: выбор из 192/384/768 кГц (дефолт 384). Бюджет Ethernet: поток DDC ≈ rate × 6 байт (24-бит I+Q) → 384 кГц ≈ 18 Мбит/с; 3 доп. пана + главный на 1.536 МГц укладываются даже в 100 Мбит с запасом, но кап «до 4 панов» держим жёстко (и по CPU тоже). Ретюн пана N>0 = запись `FDDCFreq[ddc]` + `SendFullHP` (без реконфига DDC); смена rate/ADC = `ConfigureDDCs` (как сейчас при смене sample rate). **СТАТУС 3.1 (2026-07-11): РЕАЛИЗОВАНО, собирается (ewsdr qt6 + ewsdrd), на эфире не проверено (проверяемо только с 3.2/3.3 — панов ещё никто не создаёт).** Отличие от текста плана: per-DDC состояние (freq/rate/ADC/enable) живёт в `THPSDRNetwork` (`FPanDDC*`, пишет `SetPanDDC`/`SetPanDDCFreq` из `TRadioBackend`), а контроллер держит только per-pan маппинг — эквивалентно `FDDCFreq[]` из плана, но HP-кадр собирается из одного места. Сделано: - `TBackendCaps.MaxPans`/`IndependentPanFreq` (HPSDR 4/True, Pluto 1/False); `SetPanDDC`/`SetPanDDCFreq` — виртуальные no-op в базе, override в HPSDR. - `HPSDRNetwork`: `ConfigureDDCs` → кэш главного DDC + `RebuildDDCSpecific` (главный + активные пан-DDC, каждый со своим rate/ADC); `SendFullHP` пишет частотные слова всех активных пан-DDC (`Buf[9+ddc*4]`); хелпер `DDCBaseIndex` (дедуп трёх списков плат); сброс пан-состояния в `Connect`. - `RadioController`: `MAX_PANS=4`, `PAN_DDC_RATES=(192,384,768)`; `AddPanDDC/RemovePanDDC/SetPanDDCFreq/PanDDC{Active,Freq,RateKHz}` (аллокатор hw-индексов от `FActiveDDC+1`, обратный маппинг `FDDCPanMap`); `OnDDCIQ` роутит: активный DDC → главный тракт, пан-DDC → `WDSPEngine.PushPanIQ` (пока заглушка-дроп, тело — 3.2); паны гасятся в `StopRunning`, пул сбрасывается в `StartRunning`. - ⚠ Попутный фикс: `StartRunning` не включал Saturn (BoardType 10) в список плат с DDCBase=2 → на Saturn гейт `FActiveDDC` ждал DDC0. Исправлено (`[3,4,5,10]`, как в `DDCBaseIndex`). - Частоты панов — сырые Гц железа: XVTR-трансляция на паны НЕ распространяется (XVTR — свойство главного тракта; решить при 3.3/3.4, если понадобится). ### 3.2 — WDSPEngine: анализатор и IQ-тракт на панадаптер Сейчас: анализаторы `RX_DISP_ID=0` / `TX_DISP_ID=1` (`WDSPEngine.pas:133`), создание `XCreateAnalyzer` (`:1140`), настройки+span-clip `ApplyRXAnalyzerSettings` (`:1163-1211`), снапшот спектра `Spectrum0(1, RX_DISP_ID, …)` из DSP-потока (`:1597`), выдача пикселей через `FOnSpectrum` (`:2850`). Слайсы едят копию ГЛАВНОГО аккумулятора в `ProcessSlices`. Дельты: 1. **Пан-анализаторы**: `PAN_DISP_BASE = 8` (не 4/5 — они заняты в ветке `feature/qo100-zoom-panel`, при будущем merge не столкнёмся); `XCreateAnalyzer(PAN_DISP_BASE+PanId)` при создании пана, `ApplyPanAnalyzerSettings(PanId)` — параметризованная копия `ApplyRXAnalyzerSettings` (свой rate, свой span-clip/zoom, свои FFT/avg/detector — можно стартовать с общих настроек, разделить позже). 2. **Per-pan аккумулятор**: `TPanChan = record` (`FPanAccI/Q`, `FPanAccPos`, rate, disp id, активные слайсы). `PushPanIQ(PanId, Data)` из сетевого потока кладёт в аккумулятор пана; по накоплению блока в DSP-потоке: копия → `Spectrum0(1, PAN_DISP_BASE+PanId, …)` + `ProcessSlices` по слайсам ЭТОГО пана. 3. **Слайс знает пана**: в `TSliceChan` добавляется `PanId`; NCO shift слайса = `TargetHz − PanCenterHz`; rate канала слайса = rate его пана (для панов N>0 это 192-768 кГц — WDSP-каналу дешевле, чем полный rate!). `RefreshSliceShifts` обобщается на «центр пана», гейт `SliceFitsCapture` — на «span пана». 4. **Выдача пикселей**: `FOnPanSpectrum(PanId, Pixels, N)` (или существующий колбэк + PanId), MainForm-тик раздаёт по `FPans[PanId]`. 5. **Пул WDSP-каналов** общий: 0=RXA (слайс A пана 0), 1=TX, слайсы 2..7 (`SLICE_CHAN_BASE`, `MAX_SLICES=6`) — на все паны суммарно. Этого хватает: 6 доп. слайсов + главный = 7 приёмников. Блокировки: `FSliceLock` уже сериализует слайсы vs DSP; добавить такую же дисциплину для add/remove пана (сетевой поток пишет в аккумулятор пана — создание/удаление пана только под локом, как переоткрытие слайсов при ChangeSampleRate). **СТАТУС 3.2 (2026-07-11): РЕАЛИЗОВАНО + СМОУК-ТЕСТ БЕЗ ЖЕЛЕЗА ПРОШЁЛ ПОЛНОСТЬЮ.** Отличия/уточнения против текста плана: - Пан-пакеты идут в ОБЩУЮ очередь IQ с меткой `TIQQueueItem.PanId` (0 = главный) — порядок прихода сохранён, один семафор; `IQ_QUEUE_SIZE` 64→256. Диспетчер в `TDSPThread` → `PushPanItemToDSP` (декод + feed анализатора + аккумулятор + `ProcessSlicesFor(PanId)`), всё под `FSliceLock`. - `MAX_PANS=4` переехал в WDSPEngine (единый для движка/контроллера/UI); `PAN_DISP_BASE=8` (id панов 9..11). - Блок пана `BufSize = FAudioBufSize·Rate/FAudioRate` ⇒ выходной аудио-блок слайсов ЛЮБОГО пана всегда `FAudioBufSize` (буферы не перевыделяются); `AllocSliceBuffers` берёт максимум блока по всем панам. Rate пана обязан быть кратен 48к (гейт в `AddPan`). - `TSliceChan.PanId` + `AddSlice(..., APanId)`; `OpenSliceChannel` открывает канал от rate/блока СВОЕГО пана; `SliceDSPRateFor(Mode, InRate)` — WFM-слайс поднимается от rate пана. Слайсы панов переживают `ChangeSampleRate` главного (пересоздаются в Open, как раньше). - Анализатор пана: `CreatePanAnalyzer`/`ApplyPanAnalyzerSettings` (без span-clip/zoom — пан-зум в 3.3+), живёт в OpenAnalyzer/CloseAnalyzer (пересоздание при смене rate главного). Смена детектора/усреднения/FFT/ пикселей главного перекатывается на паны (`ReapplyPanDisplaySettings`). - Выдача пикселей: `OnPanSpectrum`/`OnPanWaterfall(PanId, Pixels, N)` из display-потока; `GetPixels` пана под `FSliceLock` (гонка с RemovePan), колбэки — вне лока. - Контроллер: `TCtrlSlice.PanId`, `AddSlice(..., APanId)`, гейт/`CapturedSpan` per-pan, `SetSliceTarget`/`RefreshSliceShifts` относительно центра СВОЕГО пана, ретюн пана двигает NCO его слайсов; `AddPanDDC` создаёт пан в движке (до включения DDC), `RemovePanDDC` убирает слайсы (со звуковухами) + движок + DDC. - Смоук-тест (scratchpad `pantest.pas`, консольный, libwdsp): AddPan-валидации, спектр/водопад пана 384к с тоном +52к — пик ровно на месте (пиксель 650/651), слайс пана (USB, NCO +51к) выдал аудио-тон 1043 Гц RMS 0.68, главный тракт параллельно жив (пик −30к на месте), RemovePan мгновенно глушит слайс, без крэшей. Попутно зафиксирована конвенция синтетического IQ: «+Гц на дисплее» = I=cos, Q=−sin (полезно для будущих тестов). - НЕ проверено на эфире (нужен UI 3.3, панов никто не создаёт из GUI). ### 3.3 — UI: второй панадаптер в главном окне - **Стек панов**: область спектра = вертикальный стек `TPanafallPanel` с `TSplitter` между ними (Flex-стиль). Пан 0 сверху. Высоты в персист. - **Шапка пана** (тонкая полоска над спектром, темизирована как флаги): частота центра · диапазон · rate · бейдж ADC `A1/A2` (3.4) · кнопки `⧉` (pop-out, 3.5) и `×` (закрыть; у пана 0 нет). Шапка пана 0 появляется только когда панов ≥ 2. - **Добавить пан**: кнопка `+` в шапке/тулбаре (только если `caps.MaxPans > 1`): диалог/дефолт — частота (ввод или «текущая ±»), rate, ADC. Создание пана автоматически создаёт его первый слайс в центре. - **Активный пан** = пан активного слайса. Клик по канве пана делает его слайс активным (существующая механика `FActiveFlag`), левая панель / FreqDisplay / CAT правят активный слайс — как сейчас. - **Ctrl+ЛКМ** на любом пане = добавить слайс в этот пан (буква из глобального пула). - **Тик/FPS**: общий таймер, кадры раздаются каждому пану; FPS-гейт из перф-работ применяется per-pan. Свёрнутый/скрытый пан кадры не рендерит. - **TX**: TX-бейдж на флаге работает с любого слайса любого пана — DUC независим от DDC, TX-частота = частота слайса. (Полный мультислайс-TX — отдельная старая задача, тут ничего нового не ломаем.) **СТАТУС 3.3 (2026-07-11): БАЗОВЫЙ UI РЕАЛИЗОВАН, собирается (GUI+демон), бэкенд-тракт проверен END-TO-END против эмулятора hpsdrsim -hermes -P2 (scratchpad pantest2: DDC1 включается, IQ течёт, спектр/водопад пана рисуются, слайс живой S-метр −95дБм, ретюн/снятие чистые, seq-ошибок нет). НА ЭКРАНЕ НЕ ПРОВЕРЕНО.** Что сделано: - `TPanafallPanel`: `PanId`, регион в стеке (`StackTop/StackHeight`, Layout считает внутри региона), шапка (TPanel+TLabel+«×»→OnPanClose; видна при панах ≥2), кнопка «⊞» в ряду пан/зума (`AddPanEnabled`, только пан 0), `ShowZoomRow=False` у панов N (их зум позже), pan-aware `ViewWindow` (SliceAtPixel/LayoutFlags от окна СВОЕГО пана), `FOnFlagsInvalidate(Self)`. - MainForm: `FPans[0..MAX_PANS-1]` (пан 0 = FPan), `LayoutPanStack` (доли `FPanShare` + межпанные сплиттеры, пересчёт по MouseUp), «⊞» → `OnAddPanClick` (AddPanDDC на частоте активного VFO, rate 384к, автослайс в центре, копия display-параметров view, ApplyDarkTheme), «×» шапки → отложенное `ClosePanadapter` в тике (нельзя free из своей кнопки). - Пиксели: `engine.OnPanSpectrum/OnPanWaterfall` → MainForm напрямую (display-поток, `FPanCbLock` закрывает гонку с созданием/закрытием пана); рендер в SpectrumTimerTick по dirty-флагам, `SyncPanViews` держит центр/span вьюх и текст шапок. - Мышь панов N (НОВЫЕ простые обработчики, VFO/CTUN/маяк/оверлеи главного сюда не переносились — решение по B2 выполнено): Ctrl+ЛКМ = слайс в этом пане, ЛКМ по маркеру = drag слайса (кламп полосой пана), ЛКМ по фону = drag ЦЕНТРА пана (live-ретюн DDC), колесо над флагом любого пана = тюн того слайса; флаги/CurSliceId/инвалидции обобщены на все панели (PanOfOverlay/PushFlagStateAllPans/LayoutFlagsAllPans). - Паны живут только в Run-сессии: на STOP UI убирает их вместе с контроллером. - Отложено: пан-зум панов N, per-pan детектор/усреднение, персист (после 3.3), wideband при маленьком регионе пана 0 может вылезти за регион (редкий кейс), колесо над фоном пана N крутит главный VFO (как везде вне слайсов). **ФИКСЫ ПО ПЕРВОМУ ЭКРАННОМУ ТЕСТУ (2026-07-11, фидбек юзера):** 1. **Сплиттер спектр/водопад был сломан при ≥2 панах**: drag-математика (`SplitterMouseMove/Up`) считала доступную высоту от ВСЕГО родителя (`FParent.ClientHeight`, `TopOff=0`), игнорируя регион пана в стеке, шапку и wideband. Симптомы: у пана 0 ratio занижался (спектр сжимался при тяге вниз), у пана 1 сплиттер прыгал в регион пана 0. Фикс: Layout запоминает `FSplitTopOff/FSplitAvailH` (геометрия «спектр+водопад» СВОЕГО региона), drag использует только их. 2. **Межпанный сплиттер** не давал обратной связи до отпускания — теперь живой «призрак» (сам сплиттер едет за мышью, клампится 15%-минимумом обоих панов, подсветка при захвате), доли пересчитываются по MouseUp от финального положения призрака. 3. **Клик по фону пана N не перестраивал частоту** (только drag центра). Теперь пресс без движения (<4px) = клик-тюн `PanClickTune`: слайс пана (активный, если он этого пана, иначе первый; нет — создаётся) на частоту клика со снапом 100 Гц и клампом полосой пана. Drag центра включается после гистерезиса 4px, курсор `crSizeWE` — только с этого момента (раньше менялся на голый пресс). 4. **Диспетчер флагов/маркеров на панах N** получал координаты ВОДОПАДА (прямоугольники флагов — в системе спектра): клики в верхней части водопада могли молча съедаться невидимым флагом. Теперь флаги/hover — только для событий спектра, курсор на водопаде не трогаем. 5. **(2-й раунд фидбека)** У панов N сплиттер спектр/водопад «залипал призраком» (спектр не пересчитывался, при следующем drag прыжок к старому ratio): в `OnAddPanClick` не был подвешен `OnSplitterMoved` (у пана 0 = `RightPanelResize` → полный re-layout). Подвешен тот же обработчик. Пан 0 и межпанные сплиттеры юзер подтвердил рабочими. 6. **Курсор-«рука» на всей площади панов N (разгадан, 3-й раунд)**: юзер заметил, что рука = курсор кнопки «⊞». Механизм: GL-канвы — QOpenGLWidget с `WA_NativeWindow` = отдельная Wayland-поверхность; `crDefault` в LCL означает «курсор не задан», и поверхность НАВСЕГДА наследует курсор, бывший под мышью в момент создания виджета. Пан N создаётся кликом по «⊞» (`crHandPoint` у TFlatButton) → рука прилипает; пан 0 создаётся на старте (мышь нигде) → норм. Фикс: `Build` ставит спектру/водопаду ЯВНЫЙ `crArrow` (реальный push), обработчики панов N восстанавливают `crArrow`, не `crDefault`. ⚠Урок на 3.5 (pop-out) и вообще: любой динамически создаваемый нативный GL-виджет должен получать явный курсор при создании. Статус: все фиксы подтверждены юзером на экране, закоммичено 95b4f74 (+ dae0c19 банд-столл отдельно). **RATE ПАНОВ (реализовано 2026-07-11, после коммита 3.3):** Решение (обсуждено с юзером): (1) при создании пан наследует rate главного с клампом ≤768к (48/96/192к как есть; вне сетки → 384к); (2) rate пана независим и меняется кликом по «NNN kHz» в шапке пана → меню полной сетки железа; (3) за сменой rate главного НЕ следует (издержки Ethernet/CPU, churn). - `WDSPEngine.SetPanRate(PanId, RateHz)`: под FSliceLock — DestroyPanAnalyzer → пересбор аккумулятора (BufSize/AccI/AccQ) → CreatePanAnalyzer → переоткрытие слайс-каналов пана (Close+OpenSliceChannel, state слайсов сохраняется). - `RadioController`: PAN_DDC_RATES (192/384/768) УДАЛЁН — валидация `PanRateAllowed` = Caps.RatePresets ∩ кратные 48 кГц (полная сетка HPSDR 48-1536к). `SetPanDDCRate`: движок → FPanRateKHz → `SetPanDDC` (реконфиг DDC тем же RebuildDDCSpecific+SendFullHP) → кламп слайсов к новой полосе. `FPanADC[]` — ADC пана теперь хранится (нужен реконфигу; база под 3.4). - UI: rate в шапке — отдельный TLabel (`SetHeaderRate`, у панов N кликабелен, акцентный цвет, crHandPoint), `OnPanRateClick` → TPopupMenu из RatePresets (текущий отмечен) → `PanRateMenuItemClick` → SetPanDDCRate + SyncPanViews + PushAllSliceFlagStates + LayoutFlags. Пан 0 показывает rate некликабельно (его rate — существующий SampleRateOverlay). - ★СМОУК-ТЕСТ БЕЗ ЖЕЛЕЗА ALL PASS (scratchpad panratetest.pas, паттерн 3.2): синтетический тон +10.8кГц → пан 384к: пик спектра на месте, слайс 800Гц RMS 0.33 → SetPanRate 192к → пик/аудио живы → 768к → живы → RemovePan чисто. НА ЭКРАНЕ НЕ ПРОВЕРЕНО, НЕ закоммичено. Смена rate пана на эфире = короткий разрыв IQ этого пана (реконфиг DDC) — ожидаемо. ### 3.4 — ADC-селектор панадаптера (+ антенны Alex, уточнено 2026-07-11) Источник сигнала пана — ДВЕ независимые ручки, не одна: 1. **АЦП (per-pan)**: `TPanafall.ADCSource: 0/1`; в шапке пана селектор/бейдж `A1`/`A2`. Показывается только при `NumADCs=2` (детект платы уже есть — `NumADCs` в ConfigureDDCs-пути). Переключение = `ConfigureDDCs` с новым ADCSource для DDC этого пана. Пан 0 тоже можно перекинуть на ADC1 — это закрывает сценарий старой ветки `feature/rx2-support` как частный случай. 2. **Антенна первого АЦП (глобальная, НЕ per-pan)**: у плат с Alex на ADC0 уже есть аппаратный селектор RX-антенны **ANT1/ANT2/ANT3** (+ EXT-входы) — реализован per-band в Antenna/Alex-табе (`Alex.RxAnt[band]`, XVTR-override). Селектор один на весь ADC0 ⇒ антенну нельзя сделать свойством пана: смена антенны меняет её для ВСЕХ панов, сидящих на ADC0. В UI пана максимум — бейдж текущей антенны (например `A1·ANT2`) и/или шорткат к глобальному переключателю с явным пониманием, что это общая ручка. ADC1 (RX2-вход) — отдельный разъём без Alex-коммутации. - Diversity (sync-DDC, оба АЦП когерентно в один канал) — НЕ здесь; протокол умеет, дизайну панов не мешает, отдельная будущая фича. **СТАТУС 3.4 (2026-07-11): ГОТОВО, коммит 5e50cba, юзер подтвердил беглым тестом. ⚠Ожидаются вопросы юзера по пункту «смена rate не сбрасывает АЦП».** - `Caps.NumADCs` (HPSDR: 2 для BoardType 4/5/10 — ORION/ORION2/SATURN, те же платы, что Pkt.NumADCs в RebuildDDCSpecific; Pluto: 1; достоверно после Connect). - Контроллер: `PanADC(PanId)` / `SetPanADC(PanId, ADC)`. Пан 0 = главный DDC (`FMainADCSrc` + ConfigureDDCs с тем же rate/dither/random; FMainADCSrc теперь прокинут и в оба штатных вызова ConfigureDDCs — смена rate не сбрасывает АЦП). Паны N = `FPanADC[]` + SetPanDDC (RebuildDDCSpecific). - UI: бейдж «A1»/«A2» в шапке КАЖДОГО пана (вкл. пан 0 — закрывает сценарий rx2-support), виден только при NumADCs>1, клик = тумблер, акцентный цвет. - ⚠Ограничение: шапка пана 0 видна только при ≥2 панах ⇒ с единственным паном перекинуть пан 0 на ADC2 из UI нельзя (добавь пан или жди персиста). Антенны Alex (ANT1/2/3) — по-прежнему глобально в Antenna-табе. ### 3.5 — Pop-out: панадаптер в отдельном OS-окне Улучшение поверх Flex (SmartSDR так не умеет). Кнопка `⧉` в шапке. - `TPanFloatForm` — простая TForm-обёртка, в которую переезжает `TPanafallPanel`; `×` окна возвращает пан в стек главного окна. - **Риск №1 — GL + reparent**: перенос живого `TOpenGLControl` между окнами на Qt6/Wayland может убить контекст. Безопасный путь: НЕ репарентить канву, а пересоздать вьюху в новом окне (панель отдаёт state: центр/span/wf-историю можно не сохранять — водопад начнётся заново; слайсы/флаги живут в контроллере и переподключаются). Сначала попробовать честный reparent, fallback — пересоздание. - **Риск №2 — общие GL-ресурсы**: закрывается аудитом из 3.0. - Персист: флаг «пан N в отдельном окне» + геометрия окна. **СТАТУС 3.5 (2026-07-11): ГОТОВО, коммит 5e50cba, юзер подтвердил беглым тестом — честный GL-reparent на Qt6/Wayland РАБОТАЕТ (fallback-пересоздание не понадобилось).** - Кнопка «⧉» в шапке панов N>0 → `PopOutPan`: `TForm.CreateNew` (Caption «EWSDR · PAN N», тема, OnMouseWheel = FormMouseWheel — колесо над флагами/фоном работает как в стеке), `TPanafallPanel.ReparentTo(F)` (Parent всех контролов), StackTop/Height=0/0 (= всё окно), сплиттер пана скрыт, LayoutPanStack пере-стекует оставшихся (плавающие скипаются). - GL: reparent пересоздаёт хэндлы канв → контексты умирают. По прецеденту MSAA: `ResetGLCache` (обнуление id без glDelete) — расширен, теперь сбрасывает и водопад (`TWaterfallViewOpenGL.ResetGLCache`, новый); история водопада живёт в CPU-буфере — тексура перезальётся. - [×] окна = dock back (`CloseAction := caNone` + `DockBackPan`: reparent в PanelRight, сброс GL, Release окна). «×» шапки в окне = закрыть пан целиком (ClosePanadapter освобождает и окно). STOP закрывает всё. - Резерв на случай мёртвого GL после reparent: пересоздание вьюхи/контролов (в коде НЕ делалось — сначала экранный тест). - Персист геометрии — вместе с общим персистом панов. ### 3.6 — Pluto+ (2r2t): второй пан на втором антенном входе Опционально, после 3.0-3.3. Обычного Pluto НЕ касается (там один RX разведён). - **Детект**: число RX-каналов в iio-контексте (`voltage0/1` + `voltage2/3` у `cf-ad9361-lpc`); прошивка должна быть с `adi,2rx-2tx-mode-enable` (на Pluto+ обычно так). Если каналов 2 → `caps.MaxPans=1`, как сейчас. - **PlutoBackend** (`PlutoBackend.pas:222-236`): сейчас RX-поток пушит `FOnDDCIQ(0, pkt)`. В 2r2t буфер отдаёт 4 канала интерливом — деинтерлив на два пакета → `FOnDDCIQ(0, …)` и `FOnDDCIQ(1, …)`. Дальше это обычные псевдо-DDC, роутинг из 3.1/3.2 работает без изменений. - **Частотная связка (главное отличие)**: оба RX на одном LO и одном rate. `caps.IndependentPanFreq = False` → пан 1 создаётся ТОЛЬКО с центром/rate пана 0; ретюн любого пана ретюнит общий LO (двигает оба); в шапке пана 1 — признак связки (`🔗 LO`), контролы центра либо скрыты, либо честно тюнят общий LO. Зум/span-clip у каждого пана свой (анализаторы независимы). - **Пропускная способность**: ×2 потока. Через гигабитный Ethernet Pluto+ (наш libiio network context) проходит свободно; на USB2 пришлось бы резать rate — не наш кейс. - **QO-100 beacon-lock**: лок правит общий LO — оба пана едут вместе, инвариант «всё стоит на месте» сохраняется, конфликта нет. - **Бонус на потом**: каналы фазокогерентны — готовая база под честный diversity (сложение с фазовращателем) и пеленгацию. Не в этом этапе. - TX2 (второй выход, общий TX LO) — вне этого плана. --- ## Персист (собрать в один заход после 3.3) В `Settings.pas`: - список панов: центр, rate, ADC, span/зум, высота в стеке, pop-out+геометрия; - список слайсов: PanId, TargetHz, mode/filter/AGC/DSP, звуковуха (DeviceIndex + имя для повторного матчинга), громкость/mute; - восстановление на старте: сначала паны (ConfigureDDCs), потом слайсы. Это закрывает и старый долг «персист слайсов» из Этапа 2. --- ## Оговорки / риски - **CPU**: слайс на пане N>0 дешевле слайса на пане 0 (WDSP-канал жуёт 192-768 кГц вместо полного rate). Дорогое место — рендер: каждый пан = свой спектр+водопад; FPS-гейт и перф-дисциплина из [[project_perf_profiling]] обязательны per-pan. - **Не портить входной IQ-буфер** между каналами (fexchange0 in-place) — правило Этапа 1, действует и для пан-аккумуляторов (копия до Spectrum0, как `FSpecBuf`, см. `WDSPEngine.pas:235`). - **Старт/стоп WDSP-каналов на лету** — задержки RXA стоп/старт (`WDSPEngine.pas:~230`), создание/закрытие пана держать вне аудио-глитчей. - **Ethernet**: кап 4 пана суммарно; следить за суммарным rate при выборе rate панов. - **DDCBase=2** на PureSignal-платах: доп. панам достаются DDC3+ — при 4 панах на Angelia/Orion/Saturn это DDC2..5, ок. - **Qt6/Wayland + GL**: reparent канвы (3.5) и per-instance ресурсы (3.0) — два главных технических риска всего этапа; аудит 3.0 делать первым. - Смена sample rate главного пана переоткрывает его слайсы (уже реализовано) — панов N>0 это касаться не должно (их rate независим). ## Открытые вопросы (решить по ходу, не блокеры) - CAT: что репортить как VFO A/B при активном слайсе на пане N>0 (сейчас CAT смотрит на главный тракт; вероятно «активный слайс = VFO A»). - Перетаскивание слайса МЕЖДУ панами (drag флага на другой пан): вкусно, но не в первую итерацию — сначала слайс живёт в своём пане (кламп по span пана, как сейчас по захвату). - Авто-promotion (слайс уехал за край пана → предлагать новый пан) — нет, осознанно: только явное создание пана (решение «сначала статика» остаётся). - Настройки водопада (палитра/AGC/avg) per-pan или глобально — стартуем с наследования глобальных, разделим по запросу. - **Компоновка панов сеткой** (запрос юзера 2026-07-11): не только вертикальный стек — например, пан 0 сверху во всю ширину, паны B и C ниже бок о бок (пол-ширины каждый), и т.п. Идея на после 3.3/3.5: обобщить `LayoutPanStack` до грид-раскладки (регион пана = прямоугольник, а не полоса; `StackTop/Height` → `Rect`), сплиттеры по обеим осям. Не блокер текущего этапа. ## Отвергнутые альтернативы (для истории) Старый черновик Этапа 3 предлагал «off-span слайсы»: слайс вне видимой панорамы на своём hard-DDC, представленный edge-маркером у края спектра (цветная буква + ◄/► + частота) и/или «slices-рейлом» (полоса-менеджер с чипами слайсов). Отвергнуто 2026-07-10 в пользу Flex-модели «DDC = панадаптер»: off-span слайс без своего спектра слепой (не видно обстановку вокруг), сущности расползаются (слайс с DDC ≠ слайс без DDC), а рейл дублирует флаги. Идея рейла может вернуться позже как компактный обзор всех слайсов — но не как замена панадаптеру. Палитра «цвет+буква = личность слайса во всех местах» из черновика — остаётся в силе. ## Связанные заметки памяти - project_slices_plan (статус), project_pluto_integration, project_perf_profiling - project_spectrum_zoom (GetViewWindow / span-clip / маппинг мыши) - project_qo100_zoom_panel (прецедент доп. анализаторов WDSP, disp id 4/5!) - project_lazopengl_component (QOpenGLWidget/контексты на Qt6 — база для 3.5)