Files
ewsdr/doc/SLICES_PLAN.md
T
ew8bakandClaude Fable 5 e5921b8196 feat(slices): независимый rate панадаптеров (селектор в шапке)
- Создание пана: rate наследуется от главного с клампом ≤768к
  (вне сетки → 384к); за сменой rate главного паны не следуют
- Клик по «NNN kHz» в шапке пана N → меню полной сетки железа
  (PanRateAllowed = Caps.RatePresets ∩ кратные 48 кГц; захардкоженный
  PAN_DDC_RATES удалён)
- WDSPEngine.SetPanRate: под FSliceLock пересбор аккумулятора/анализатора
  пана + переоткрытие слайс-каналов на новом rate (state сохраняется)
- RadioController.SetPanDDCRate: движок → реконфиг DDC (SetPanDDC) →
  кламп слайсов к новой полосе; FPanADC[] хранится (база под 3.4)
- Шапка: rate отдельным лейблом (у панов N кликабелен, акцент/рука)

Смоук-тест без железа ALL PASS (тон: спектр+аудио слайса живы через
384→192→768). Проверено юзером на экране.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 23:28:25 +03:00

48 KiB
Raw 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.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.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/HeightRect), сплиттеры по обеим осям. Не блокер текущего этапа.

Отвергнутые альтернативы (для истории)

Старый черновик Этапа 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)