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

68 KiB
Raw Permalink Blame History

Мультислайсы и панадаптеры (как во 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.
  • RadioControllerTCtrlSlice (абс. 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 все текстуры (FGridLabelTexFLetterTex, 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. Диспетчер в TDSPThreadPushPanItemToDSP (декод + 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 и PanViewWindowGetPanViewWindow ⇒ вся мышь/флаги панов автоматически зум-корректны. Темизация баров панов 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/HeightRect), сплиттеры по обеим осям. Не блокер текущего этапа.

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)