Files
ewsdr/doc/SLICES_PLAN.md
ew8bakandClaude Opus 4.8 3566f10191 feat(slices): per-pan дисплей — палитра/gamma/уровни на пан («◑»-поповер)
Кнопка «◑» в шапке каждого пана открывает компактный поповер настроек
отображения (палитра, gamma, ref/range спектра, авто-уровень водопада +
offset/ручные min-max). Модель «глобальный дефолт + override на пан»: пан 0
правит глобальные поля рендера (зеркалятся в контроллер), паны N ставят
DisplayOverride и персистятся в воркспейс раздельно КВ/трансвертер.

- PanDisplayPopup.pas (new): borderless fsStayOnTop форма, гаснет по
  OnDeactivate; Flat*-контролы; крестик «×» закрытия; дим-хинт «правки
  применяются и сохраняются сами»; кнопки «⟳ Reset»/«⇄ Copy to all».
- TPanafallPanel: FBtnDisplay + OnDisplayClick + DisplayOverride;
  PositionHeaderChildren каскадит [×][⧉][◑] справа по Visible.
- MainForm: OnPanDisplayClick/Applied/Reset/ApplyAll + ApplyGlobalRenderToView
  + MarkPanDisplayDirty. При смене палитры/сбросе чистим историю GL-водопада
  (TSpectrumView.ClearWaterfall) — GL хранит уже раскрашенные пиксели.
- Settings/TPanCfg: под-объект 'display' (палитра/gamma/ref/range/wf-уровни),
  пишется только при override.

Глобальными осознанно оставлены разрешение анализатора, MSAA, скорость
водопада, детектор/усреднение (движок/перф, не косметика).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 13:49:19 +03:00

783 lines
68 KiB
Markdown
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.
# Мультислайсы и панадаптеры (как во 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): ГОТОВО, коммит 8bf8bcf, юзер подтвердил беглым
тестом. ⚠Ожидаются вопросы юзера по пункту «смена 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): ГОТОВО, коммит 8bf8bcf, юзер подтвердил беглым
тестом — честный 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: пересоздание вьюхи/контролов
(в коде НЕ делалось — сначала экранный тест).
- Персист геометрии — вместе с общим персистом панов.
**СТАТУС «хирургический ChangeSampleRate» (2026-07-12): ГОТОВО — смоук-тест
ALL PASS, юзер подтвердил на экране («всё работает»). Отладочные пробы
RATEPROBE удалены.** Наблюдение юзера: смена rate ГЛАВНОГО пана
коротко замораживала водопады панов N (обратное — нет). Причина была в движке:
`ChangeSampleRate` делал полный Close/Open (стоп DSP-потока, разрушение ВСЕХ
анализаторов вкл. пановых, закрытие всех слайс-каналов, сброс общей IQ-очереди).
Протокол чист: оба пути шлют одинаковый полный DDC Specific (`RebuildDDCSpecific`).
Сделано (WDSPEngine.pas + RadioController.pas):
- Гейт `FMainGated` + барьер `FMainLock`: DSP-поток при взведённом гейте дропает
main-пакеты (PanId=0) из общей очереди НЕ блокируясь, паны обрабатываются без
пауз; двойная проверка гейта (снаружи и под локом) закрывает гонку установки.
- `ChangeSampleRate` теперь хирургический: закрывает/переоткрывает ТОЛЬКО
RXA_CHAN + слайс-каналы пана 0 (их InRate = главный rate); DSP/display-потоки,
TXA, TX-поток, анализаторы и паны не трогаются. RX-анализатор переармируется
`ApplyRXAnalyzerSettings` (как при зуме) — история главного сбрасывается
(неизбежно), пановые НЕ. Ошибка реконфига → `FInitialized:=False` (контроллер
увидит по `Initialized`).
- Общий код RXA вынесен: `OpenMainRXChannel` (открытие+реставрация состояния из
полей движка, вкл. FShiftHz) / `CloseMainRXChannel` — используются Open/Close/
ChangeSampleRate.
- Контроллер `SetSampleRate` больше НЕ сбрасывает `FWDSPReady` на время смены —
иначе OnDDCIQ переставал кормить паны (это был второй источник паузы).
- ★Находка по пробам: `SetChannelState(chan,0,dmode=1)` на ПРОСТАИВАЮЩЕМ канале
зря ждёт флаш ~130мс. Каналы при закрытии всегда idle (слайсы — дисциплина
FSliceLock, RXA — гейт-барьер) → dmode=1→0 в `CloseSliceChannel` и
`CloseMainRXChannel`. Итог: смена rate 331→120мс, макс. пауза аудио слайса
пана 137→40мс (~2 блока, на глаз незаметно). Бонус: закрытие слайсов/панов и
SetPanRate тоже ускорились на ~130мс/канал.
- Смоук-тест scratchpad `ratetest.pas` (синтетический тон, фидер-поток, замер
межблочных пауз аудио пан-слайса вокруг смены 384к→192к): ALL PASS — пан без
паузы >41мс, main-аудио/слайс пана 0/слайс пана 1 живы и слышат тон после
смены. Оба бинарника собираются.
**СТАТУС «пан-зум панов N» (2026-07-12): ГОТОВО — смоук panzoomtest ALL PASS,
юзер подтвердил на экране, коммит cb6bdfa.**
- Движок: `TPanChan.ZoomFactor/PanSlider`, span-clip в
`ApplyPanAnalyzerSettings` (та же математика, что у главного; ZOOM_LIMIT 100),
`SetPanZoomPan` (no-op guard — SetAnalyzer сбрасывает FFT-историю, урок
dae0c19). Зум переживает смену rate пана (SetPanRate пересоздаёт анализатор
из записи пана).
- Контроллер: `FPanZoomF/FPanPanSl` (сессия пана, сброс в Add/RemovePanDDC —
band-персиста у панов нет), формула окна вынесена в юнит-хелпер
`ViewWindowFor` (GetViewWindow главного теперь тоже через него),
`GetPanViewWindow(0)` делегирует главному; `PanZoomIn/Out` по той же
лестнице ZOOM_MAG_LADDER, якорь = ЦЕНТР текущего окна (`PanZoomStepTo`,
у панов нет VFO-якоря главного), `PanZoomDefault`, геттеры.
- UI: `TPanZoomEvent` получил Sender (PanZoomBar), `OnPanZoomWindow`
диспетчеризует бар→пан; кнопки −/⌂/+ общие (`PanFromSender`); у панов N
включён ShowZoomRow, бар проводится в CreatePanadapter; единые
`SyncPanZoomUI/RefreshPanZoom` (вьюха+бар ← контроллер) используются в
SyncPanViews и drag-ретюне; `TPanafallPanel.ViewWindow` и `PanViewWindow`
`GetPanViewWindow` ⇒ вся мышь/флаги панов автоматически зум-корректны.
Темизация баров панов N в ApplyDarkTheme.
- Смоук panzoomtest.pas: пик тона +50к на пане 384к совпадает с формулой окна
при зуме 0 / 0.5+пан 0.7 / сбросе (±0.6%).
- ⚠Фикс по первому экранному тесту: кнопки −/⌂/+ панов N рулили ГЛАВНЫМ зумом —
MakeFlatBtn создаёт кнопки с Owner=контейнер (не панель), PanFromSender их
не находит. Диспетчер кнопок = PanOfZoomButton (матч по идентичности
BtnZoomIn/Def/Out всех панов). Урок: PanFromSender работает только для
контролов с Owner=панель (TLabel шапки — да, MakeFlatBtn-кнопки — нет).
**СТАТУС «грид-раскладка панов» (2026-07-12): РЕАЛИЗОВАНА, собирается, НА
ЭКРАНЕ НЕ ПРОВЕРЕНА, НЕ закоммичена.**
- Панель: регион пана обобщён до прямоугольника — `StackLeft/StackWidth`
(0/0 = вся ширина), Layout смещает ВСЁ по X0 (шапка/спектр/линейка/
сплиттер/водопад/ряд зума); `PositionPanZoomBar` получил X0 и 5-ю кнопку;
новая кнопка `FBtnGrid` («▦»/«▤», глифом рулит хозяин) + `GridClick`
(задать до Build) + `GridBtnEnabled`; ReparentTo переносит кнопку.
- MainForm: `FPanGridMode` (сессия), `FPanGridRowShare` (доля верхнего ряда,
0.15..0.85, дефолт 0.5), `FPanColShare[1..]` (доли колонок, init 1.0,
нормируются суммой), `FPanVSplitters[1..]` (вертикальный сплиттер СЛЕВА от
колонки пана i, crHSplit, создаётся в CreatePanadapter, free в
ClosePanadapter). `PanGridActive` = режим включён И >=2 доп. панов в стеке
(иначе автоткат в стек, режим помнится). LayoutPanStack: ветка грида — пан 0
верхний ряд во всю ширину (wideband внутри как в стеке), паны N колонками;
горизонтальный сплиттер ряда = сплиттер ПЕРВОЙ колонки во всю ширину
(PanSplitMouseDown/Up ветвятся по PanGridActive: в гриде правят
FPanGridRowShare напрямую), сплиттеры остальных колонок скрыты; вертикальные
PanVSplitMouse* — зеркало горизонтальных (призрак, min 15% ширины,
DShare=DeltaX/TotalW*ShareSum). Кнопка «▦/▤» видна при FPanGridMode или
>=3 застекованных панах (выключить можно всегда). PopOutPan обнуляет
StackLeft/Width и прячет V-сплиттер. Темизация: V-сплиттеры T.Border,
BtnGrid через StyleButton.
- Флаги/мышь/зум панов грид не трогает: всё в координатах канвы спектра,
канва целиком переезжает в регион-колонку.
- Проверить на экране: тумблер при 3 панах, оба типа сплиттеров, wideband в
гриде, pop-out/возврат из грида, закрытие пана в гриде (автоткат),
узкие колонки (зум/клик-тюн/drag), отсутствие регрессий при 1-2 панах.
**ФИКСЫ ПО ПЕРВОМУ ЭКРАННОМУ ТЕСТУ ГРИДА (2026-07-12, собирается, не
проверено):** три независимых бага из фидбека юзера.
1. **Наезд панов при выходе из грида (и вообще при 4 панах в стеке).**
LayoutPanStack делил высоту по долям (после add/close все доли равные,
при 4 панах = 25%), а Layout панели клампит спектр/водопад к минимумам
(MIN_SH=60/MIN_WH=40) — виджеты вылезали за регион, следующий пан
рисовался поверх (пан 0 терял водопад и ряд кнопок). Фикс:
`TPanafallPanel.MinLayoutHeight` (те же константы/DPI, что Layout) +
`PanMinHeight(PanId)` в MainForm (пан 0 += wideband `FPanWideH`);
раздача в стеке клампит каждый пан минимумом и режет раздутые так,
чтобы оставшимся хватило на их минимумы; в гриде Row0H клампится
минимумом пана 0 и минимумом нижнего ряда (макс. по колонкам);
драг межпанного сплиттера ограждён `Max(15%, минимум содержимого)`
в обеих ветках (стек/грид).
2. **«Блуждающие» кнопки после STOP (− − ⌂ +, кнопки на линейке, ⊞ накрыт).**
MakeFlatBtn создаёт кнопки с Owner=КОНТЕЙНЕР (не панель) → при гибели
пана (STOP → DestroyAllExtraPans → P.Free) кнопки /⌂/+/⊞/▦ НЕ
освобождались и оставались жить на PanelRight в старых координатах,
а PanOfZoomButton, не найдя пан, роутил их клики на главный зум.
Фикс: TPanafallPanel.Destroy явно освобождает FBtnZoomOut/Def/In,
FBtnAddPan, FBtnGrid (FBtnPopPan/ClosePan — Owner=FHeaderPanel,
умирают с ним). Перезапуск программы «лечил», потому что утечка —
в пределах процесса.
3. **Жор CPU (+~40% за пан).** SyncPanViews (тик, частота кадров) звал
SyncPanZoomUI с БЕЗУСЛОВНЫМ `PbPanZoom.Invalidate` на каждый пан каждый
кадр (регрессия cb6bdfa) = полный софт-рендер зум-бара + Qt-рекомпозит
@~50fps × N панов. Фикс: инвалидация только внутри
ZoomBar.SetFreq/SetState (они no-op при тех же значениях), событийный
Invalidate перенесён в RefreshPanZoom. Дополнительно жор усиливался
багом 1 (наезжающие GL-виджеты сверх окна). Замер юзера — после фикса.
Ответ на вопрос «почему только 3 доп. пана»: MAX_PANS=4 (WDSPEngine) —
пан 0 + 3 доп.; 6 было у СЛАЙСОВ (MAX_SLICES=6 доп.). Поднять можно (P2
позволяет много DDC), но сначала подтвердить CPU после фиксов.
Фиксы 1-3 ПОДТВЕРЖДЕНЫ юзером на экране 2026-07-12 («работает», CPU со
всеми панами 17-20%). Добит хвост:
4. **Пропорция спектр/водопад пана 0 не восстанавливалась после закрытия
панов (50/50 → 70/30).** Layout клампил ratio минимумами С ЗАПИСЬЮ в
FSplitterRatio: в маленьком регионе (стек панов) MIN_SH/AvailH ≈ 0.7
запоминался навсегда. Фикс: кламп в локальную EffRatio, FSplitterRatio
меняет только drag сплиттера юзером.
### 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-13
(см. блок «PER-PAN ДИСПЛЕЙ» ниже).
- **Компоновка панов сеткой** (запрос юзера 2026-07-11): не только вертикальный
стек — например, пан 0 сверху во всю ширину, паны B и C ниже бок о бок
(пол-ширины каждый), и т.п. Идея на после 3.3/3.5: обобщить `LayoutPanStack`
до грид-раскладки (регион пана = прямоугольник, а не полоса; `StackTop/Height`
`Rect`), сплиттеры по обеим осям. Не блокер текущего этапа.
**PER-PAN ДИСПЛЕЙ (2026-07-13, собирается, dispcfgtest ALL PASS, НЕ закоммичен,
НА ЭКРАНЕ НЕ ПРОВЕРЕН)**
UX залочен с юзером: кнопка «◑» в шапке каждого пана открывает компактный
поповер настроек отображения. Модель — **глобальный дефолт + override на пан**:
- Пан 0 «◑» правит ГЛОБАЛЬНЫЕ поля рендера (`FController.FWf*`/`FSpec*`),
зеркалятся в контроллер, persist глобальных при закрытии (FormDestroy →
SaveGlobalSettings). Solo пан 0 (нет доп. панов ⇒ шапка скрыта) правится
штатным диалогом настроек — «◑» виден только при видимой шапке (>=2 панов).
- Паны N при первой правке ставят `DisplayOverride:=True`; их рендер
персистится в воркспейс раздельно по контексту (КВ/xvtr).
- «Reset» (только паны N) — снять override, вернуть глоб. дефолт.
«Apply all» — раскатать текущий рендер пана на все паны + сделать глоб.
дефолтом.
Что per-pan (view-side, дёшево — всё уже в `Pan.View`): палитра, gamma,
ref level, range, водопад авто-уровень + offset + ручные min/max. Что осталось
ГЛОБАЛЬНЫМ (движок/перф, не косметика): DisplayPixels (разрешение анализатора),
MSAA, WfSpeedDiv, детектор/усреднение спектра и водопада.
Реализация:
- Новый юнит **PanDisplayPopup.pas**: `TPanDisplayPopup` = borderless
`fsStayOnTop` форма, гаснет по `OnDeactivate`; Flat*-контролы (combo палитры,
float-spin gamma, int-spin уровней, чекбокс авто). Combo-дропдаун парентится
к самой форме (child) ⇒ не деактивирует поповер. Правки применяются к
`P.View` живьём → `OnApplied(P)`.
- **TPanafallPanel**: `FBtnDisplay` («◑», Owner=FHeaderPanel ⇒ освобождается
авто с шапкой, в Destroy не нужен), `OnDisplayClick`, `DisplayOverride`.
`PositionHeaderChildren` каскадит `[×][⧉][◑]` справа налево по `Visible`.
- **MainForm**: `OnPanDisplayClick` (лениво создаёт `FPanDisplayPopup`, показ
под кнопкой), `OnPanDisplayApplied/Reset/ApplyAll`, `ApplyGlobalRenderToView`,
`MarkPanDisplayDirty` (инвалидция сетки спектра + водопада; `SetWfPalette/
Gamma` сами no-op-гардят и метят LUT к пересбору). Поповер прячется в
`ClosePanadapter` (FPan мог висеть → перепривязка на след. показе).
- **Settings/TPanCfg**: `HasDisplay` + `DPalette/DGamma/DSpecRef/DSpecRange/
DWfAGC/DWfOffset/DWfHigh/DWfLow`; под-объект `'display'` пишется ТОЛЬКО при
override; чтение = присутствие блока. Save/RestorePanWorkspaceCtx прокинуты.
Файлы: `PanDisplayPopup.pas` (new), `PanafallPanel.pas`, `MainForm.pas`,
`Settings.pas`. NEXT: экранная проверка (палитра/gamma/уровни на пан, Reset/
ApplyAll, тёплый рестарт восстанавливает override панов N раздельно КВ/xvtr).
## Отвергнутые альтернативы (для истории)
Старый черновик Этапа 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)