Files
ewsdr/doc/SLICES_PLAN.md
T
ew8bakandClaude Opus 4.8 b2fa2756cf docs(slices): draft Stage 3 notes (HW DDC design + UI concept)
Record grounded data-path analysis, per-DDC/RX2 deltas, the soft-vs-hard
WDSP channel split, preliminary decisions (4-6 hard DDCs, static first),
and the UI concept (edge markers, slices rail, ADC badge). Marked as a
work-in-progress draft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:36:31 +03:00

23 KiB
Raw Blame History

Мультислайсы (несколько приёмных фильтров, как во FlexRadio)

Статус: Этапы 1 + 2 РЕАЛИЗОВАНЫ и ЗАКОММИЧЕНЫ (ветка feature/multislice, коммит 1527b0f; проверено на эфире) + UI-полировка флагов (30a6616, 3049f91, a3ac459). Этап 3 (аппаратные DDC для слайса вне захваченной полосы) — не начат. Ниже: что вошло по факту + дизайн/остаток.

Что реализовано (Этапы 1 + 2)

  • WDSPEngine — софтовый fan-out: TSliceChan + FSlices[0..MAX_SLICES-1] (MAX_SLICES=6), свои WDSP-каналы (SLICE_CHAN_BASE=2), ProcessSlices в DSP-потоке (свежая копия IQ на канал, свой NCO/mode/filter/AGC/NR/NB/SNB/ANF + S-метр → OnSliceAudio), FSliceLock сериализует add/remove/set vs DSP, переоткрытие после смены sample rate.
  • 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+) в один ряд: каждый рядом со своей полосой фильтра, но флаги НЕ наезжают (сосед слева толкает правого; обратный поджим при выходе за правый край). PositionVfoOverlay/ PositionSliceFlags — тонкие обёртки над ним.
    • Идентификация уехавшего флага — буквой слайса по центру его полосы фильтра сверху (DrawBandLetter CPU + DrawBandLetterGL GL с кэшем текстур букв); у главного A буква только когда есть слайсы.
    • Палитра слайсов VfoOverlay.SliceColor(L) (A зелёный … F фиолетовый, не красный — не путать с TX): один цвет для бейджа-буквы во флаге и для буквы на полосе.
    • Кнопки флага — общий FlatButton.PaintFlatButton (плоские, как левая панель; без «стеклянного» градиента), центровка бейджа через TextRect.
    • Флаг полностью тематизирован: структурные CLR_* выводятся из TAppTheme (ApplyThemeColors), тема раздаётся из ApplyDarkTheme + WireOverlayEvents. Статус-акценты (TX/SPLIT) фиксированы намеренно.
  • Перф — S-метр развязан с кэшем «хрома» флага (FMeterDirty + meter-only RefreshMeter через EnsureRendered); InvalidateVfoOverlay не дёргает глобальный перезалив оверлеев на тик метра.

Остаток/риски (не сделано): персист слайсов в Settings (список + устройство); per-slice AGC-Top (сейчас общий с главным); полноценный мультислайс-TX (пока TX только главный); Этап 3 (HW DDC). Исходный план/дизайн — ниже без изменений.

Предыстория (UI-каркас до мультислайса — исторично)

Ниже — состояние ДО реализации Этапа 2 (одиночный флаг, захардкоженная 'A'). Актуальное состояние см. в «Что реализовано» выше. Оставлено как контекст.

VfoOverlay.pas переделан в «флаг слайса» в стиле SmartSDR (6 коммитов 4a92256..a04c592, уже в main). Это ровно UI-управление одним слайсом из Этапа 2, только пока в единственном экземпляре:

  • Флаг-накладка поверх спектра с буквой слайса (FSlice, сейчас захардкожено 'A'), fly-out пикерами MODE / DSP / AUD, слайдером громкости, mute, SPLIT, кликабельным TX-бейджем (выбор TX-слайса) и S-метром.
  • Per-flag выбор аудио-устройства уже есть: SetAudioDevices + OnAudioDevice (VfoOverlay.pas:31,125,136) — прокручиваемый список в AUD-fly-out. Это готовый UI под «своё звуковое устройство на слайс» из абстракции TSlice.DeviceIndex.
  • События наружу: OnSelect/OnDSPChange/OnAGCChange/OnVolume/OnMute/OnAudioDevice/ OnSplit/OnTxSelect/OnInvalidate (VfoOverlay.pas:131-139).
  • Автопозиционирование: флаг флипается влево, если не влезает справа (6ad5f29).

Привязка в приложении пока одиночная: один FVfoOverlay в MainForm (MainForm.pas:125), созданный/подключённый на MainForm.pas:962-972, привязан к VFO A (UpdateVfo(FController.FVfoA), MainForm.pas:3265); хендлеры OnVfoOverlay* шлют изменения в главный тракт. SpectrumView держит один FVfoOverlay и рисует его в DrawOverlay (SpectrumView.pas:72,220,1010).

Чтобы превратить это в мультислайс — нужно из одиночного флага сделать коллекцию флагов (по одному на слайс) и развести их события на конкретный SliceId вместо глобального VFO A. Детали — в обновлённом Этапе 2.

Цель

Несколько независимых приёмных «слайсов» на одном ADC/одном IQ-потоке: у каждого слайса своя частота, режим, полоса, AGC и своё звуковое устройство (как DAX/наушники/динамик во Flex). Панадаптер (спектр+водопад) остаётся один, слайсы рисуются маркерами поверх.

Как это устроено у Flex и что из этого берём

Слайс во Flex — это не «ещё один фильтр» на одном приёмнике, а полноценный независимый узкополосный приёмник (свой DDC в FPGA + демодулятор + фильтр + AGC + аудио). Общий у слайсов — только панадаптер (широкий FFT-захват).

Один WDSP-канал = один демодулятор = один фильтр = один аудиопоток. Значит для «разный звук/режим/полоса на каждый слайс» нужен отдельный WDSP-канал на слайс. Способ получить узкополосный IQ для канала различается по железу — см. ниже.

Ключевое различие бэкендов

openHPSDR (Protocol 2, V4.3, MAX_DDCS=80):
  ADC0 ─┬─ DDC (FPGA-тюнер) → IQ-поток порт 1035+n → WDSP-канал → фильтр/AGC → AudioOut[dev]
        ├─ DDC → ...
        └─ DDC → ...
  • Аппаратные DDC: слайс тюнится КУДА УГОДНО в полосе ADC (весь КВ), дёшево по CPU.
  • На Angelia/Orion/Saturn DDC0/DDC1 заняты PureSignal → слайсы с DDC2+ (в коде DDCBase=2).
  • RX2 = ВТОРОЙ ADC (ADC1), отдельная антенна/diversity — ОРТОГОНАЛЬНО слайсам, не нужно.

Pluto (AD9363, libiio):
  один RX-тракт: один LO, один rf_bandwidth, один IQ-поток (притворяется одним DDC,
  пушится как TDDCIQPacket в тот же конвейер).
  • Аппаратных DDC НЕТ. Слайсы = СОФТОВЫЙ fan-out одного IQ-блока на N WDSP-каналов,
    каждый со своим SetRXAShiftFreq в пределах захваченной полосы вокруг LO.
  • Для QO-100 идеально: весь транспондер (~500 кГц) в захвате, LO залочен на маяк.

Единая абстракция

TSlice = record
  Id:           Integer;
  TargetHz:     Double;        // абсолютная частота слайса
  Mode:         TRxMode;       // USB/LSB/CW/FM/...
  FilterLo,
  FilterHi:     Integer;
  AGCMode:      TWDSPAGCMode;
  Volume:       Double;
  WDSPChannel:  Integer;       // канал WDSP (0..N); 0=главный, 1=TX заняты
  Source:       (srcSoftShift, srcHardDDC);  // как получаем узкий IQ
  DDCIndex:     Integer;       // если srcHardDDC — какой аппаратный DDC
  ShiftHz:      Double;        // если srcSoftShift — сдвиг = TargetHz - CapturedCenterHz
  AudioOut:     TAudioOutput;  // свой инстанс со своим DeviceIndex
  DeviceIndex:  Integer;       // выбранная звуковуха (или -1 = default)
  Enabled:      Boolean;
end;

Правило маршрутизации по частоте (одно на оба бэкенда):

  • слайс внутри текущего захваченного span → srcSoftShift (Pluto и HPSDR);
  • слайс вне захваченного span → srcHardDDC (только HPSDR; Pluto так не умеет).

Порядок работ

Этап 1 — Софтовый fan-out в WDSPEngine (backend-agnostic) РЕАЛИЗОВАН

Даёт слайсы сразу на Pluto/QO-100 (единственный режим) и на HPSDR для слайсов внутри видимой полосы. Pluto уже пушит IQ через тот же DDC-конвейер, поэтому fan-out живёт в движке и не зависит от бэкенда.

  • WDSPEngine.pas: сейчас RXA_CHAN=0 один канал, один FRXOut, один аудиовыход (WDSPEngine.pas:96, alloc буферов ~:159, OpenChannel ~:1081).
    • Добавить массив слайс-каналов: для каждого активного слайса OpenChannel(chN, ...) с той же геометрией, что RXA_CHAN, + SetRXAShiftRun/SetRXAShiftFreq(chN, ShiftHz) (механизм уже используется: WDSPEngine.pas:1099-1100), SetRXAMode, RXASetPassband, AGC, SetRXAPanelGain1.
    • В DSP-потоке после накопления IQ-блока (FRXAccI/Q, FRXAccPos) прогонять fexchange0 по КАЖДОМУ активному каналу из ОДНОГО и того же входного буфера (входной буфер не портить in-place между каналами — держать копию, как FSpecBuf).
    • Выход каждого канала → свой callback OnSliceAudio(SliceId, L, R, Count).
  • AudioOutput.pas: уже готов к мульти-устройству — DeviceIndex, EnumOutputDevices, FindDeviceByName (AudioOutput.pas:137-151). Создавать отдельный TAudioOutput на слайс, каждый со своим DeviceIndex.
  • RadioController.pas: TSlice[] (add/remove/retune/setmode/setdevice), роутинг OnSliceAudio → нужный TAudioOutput.

Проверка Этапа 1: Pluto/QO-100 — 2 слайса (VFO + маяк-монитор), звук в две разные звуковухи, независимые режимы/полосы. Сборка: lazbuild --ws=qt6 ewsdr.lpr.

Этап 2 — UI слайсов на водопаде + выбор устройства РЕАЛИЗОВАН (кроме персиста)

Каркас флага слайса уже сделан (см. «Что уже готово»): один TVfoOverlay со всеми пикерами и per-flag выбором устройства. Остаётся размножить его на N слайсов и привязать к позиции по частоте. Выпадашка устройства и dropdown-паттерн уже есть в самом флаге (AUD fly-out) — отдельный DeviceForm не нужен.

  • Коллекция флагов вместо одного. Из MainForm.FVfoOverlay (одиночный, MainForm.pas:125) сделать array of TVfoOverlay / список, по флагу на слайс; SpectrumView.FVfoOverlay (SpectrumView.pas:72) — тоже коллекция, рисовать все в DrawOverlay (SpectrumView.pas:1010). Разместить: SetSlice(Id, Letter) в оверлей, события OnVfoOverlay* должны нести SliceId (сейчас бьют по VFO A напрямую — завести маппинг оверлей→слайс, роутить в нужный TSlice/WDSP-канал из Этапа 1).
  • Позиционирование по частоте. PositionVfoOverlay (MainForm.pas:518) сейчас ставит один флаг; расширить на маппинг TargetHz → X через GetViewWindow/ SyncSpecViewFreq (зум-работа). Слайс за краем видимой полосы — маркер у края (флип влево/вправо уже есть, 6ad5f29).
  • ЛКМ по водопаду = поставить/двинуть активный слайс; выделение активного (подсветка буквы/рамки флага). Хит-тест мыши флага уже разведён (HandleMouseDown/Move/Up, MainForm.pas:4424) — добавить «клик мимо флага по спектру = ретюн активного слайса».
  • TX-select уже кликабелен (OnTxSelect, 08c71c4) — при мультислайсе выбирать, какой слайс идёт в TX; снимать флаг с остальных.
  • Персист: список слайсов в Settings.pas (per-band или глобально — решить), включая DeviceIndex/имя устройства каждого флага.

Этап 3 — Аппаратные DDC для openHPSDR (слайс куда угодно по КВ) НЕ НАЧАТ

Апгрейд поверх общего каркаса; нужен только чтобы ставить слайс ВНЕ захваченной полосы. Плита ~на 70% есть:

  • ConfigureDDCs(NumDDCs, ...) уже включает N DDC с per-DDC конфигом/ADC-source (HPSDRNetwork.pas:220, DDCConfig[ddc*6+0]=ADCSource).
  • Приём разведён по DDC: порты 1035+nHandleDDCIQ(Buf,Len,DDCIdx) (HPSDRNetwork.pas:392).
  • High Priority уже держит частоту на КАЖДЫЙ DDC (bytes 9-12=DDC0, 13-16=DDC1, …) HPSDRProtocol.pas:271.

Чего не хватает:

  1. Per-DDC частота: сейчас SetRunAndFreq(...; DDC0FreqHz; ...) знает только DDC0 (HPSDRNetwork.pas:224) — писать частотное слово каждого DDC в High Priority.
  2. Роутинг HandleDDCIQ(DDCIdx) → соответствующий WDSP-канал слайса.
  3. Учёт DDCBase=2 (PureSignal) и Ethernet-бюджета (реально 4–8 слайсов, не 80).

feature/rx2-support — это про ВТОРОЙ ADC, не про слайсы; но её каркас (доп. WDSP-канал + доп. аудиовыход + роутинг по индексу) частично переиспользуем.

Этап 3 — рабочие заметки (черновик, обсуждение НЕ закончено)

Разобрали фактический data-path и набросали дизайн; вернёмся позже.

Что есть сейчас (по коду):

  • Один аппаратный DDC: ConfigureDDCs(1, rate, ADCSource=0) (RadioController.pas:3314), IQ на порт 1035+nHandleDDCIQ(DDCIdx)OnDDCIQ(DDCIndex). В WDSP подаётся только FActiveDDC (RadioController.pas:843), остальные DDC отбрасываются.
  • Слайсы = софт fan-out этого одного IQ: SetSliceShift(Id, TargetHz-FCenterFreq) (RefreshSliceShifts:1063). Гейт SliceFitsCapture ±47.5% rate вокруг центра (:1081); вне полосы AddSlice отказывает (:1097).
  • HP-фрейм шлёт частотное слово только DDC0: Buf[9 + DDCBase*4] (SendFullHP:1567); слова остальных DDC = нули.
  • ConfigureDDCs умеет N DDC (per-DDC enable), но пишет один rate/ADCSource на все; DDCBase=2 на Orion/Angelia/Saturn (DDC0/1=PureSignal), NumADCs=2 на 2-АЦП платах.

Что нужно (дельты по точкам):

  1. Per-DDC частота в HP: SendFullHP — цикл Buf[9+ddc*4]=FreqToPhaseWord(FDDCFreq[ddc]); контроллер держит FDDCFreq[], обновляет при ретюне слайса.
  2. Per-DDC rate + ADCSource в ConfigureDDCs (принимать массив): главный DDC широкий (панорама), слайс-DDC узкие ~48–96 кГц (дёшево по линку). Протокол это позволяет.
  3. Аллокатор DDC: пул hw-индексов от DDCBase+1, захват/освобождение, кап (см. решения).
  4. Ключевое — два типа слайс-канала в WDSPEngine:
    • soft (Этап 1): вход = копия главного IQ, rate главного, NCO shift = Target-Center, общий аккумулятор.
    • hard (Этап 3): вход = свой DDC-пакет, rate его DDC, NCO shift = 0, свой аккумулятор FSliceAccI/Q. Нужен PushSlicePacket(chan, ...) + роутинг OnDDCIQ(DDCIndex)→WDSP-канал привязанного слайса.
  5. RX2 (второй ADC): TSlice.ADCSource (0/1) → слайс-DDC на нужный ADC. Даёт двух-антенные слайсы (главный RX на ANT1, слайс на ANT2). Ортогонально «внутри/вне полосы».

Принятые решения (предварительно):

  • Лимит: до 4–6 hard-DDC слайсов, со строгим контролем суммарного rate (бюджет Ethernet).
  • Сначала статика: источник (soft/hard) выбирается при добавлении/явном ретюне; авто-promotion/demotion (soft↔hard при смене центра/зума) — отдельным этапом позже.
  • RX2/ADCSource — не решено (нужно знать, как заведены антенны у пользователя).

UI-концепция (черновик, не финал — выбор представления off-span НЕ сделан): Принцип: цвет+буква слайса = его личность во всех местах (флаг, полоса, edge-маркер, чип рейла, ADC-бейдж) — переиспользуем палитру SliceColor, тему, PaintFlatButton.

  • Слайс в полосе — как сейчас (флаг сбоку + буква на полосе, packing).
  • Слайс вне видимой панорамы (hard) — приклеенный к краю спектра edge-маркер: цветная буква + стрелка (◄/►) + абсолютная частота. Клик = выбрать, двойной = перевести главную панораму на него. Почти бесплатно (флип/клэмп к краю уже есть).
  • Slices-рейл — тонкая полоса-менеджер (под панорамой или в правой панели): чип на слайс (цветная точка+буква · частота · режим · мини-S-метр · ADC-бейдж), сортировка по частоте, [+] = добавить с вводом частоты (для off-span, где Ctrl-клик не поможет). Единственное место, где off-span слайсы всегда видны/управляемы.
  • ADC/антенна — бейдж A1/A2 в шапке флага и в чипе (только на 2-АЦП платах).
  • soft/hard — тонкий признак (плотная vs полая точка, или значок ); не кричать.
  • Позже «вау»: мини-панадаптер (свой спектр/водопад) на каждый hard-слайс в чипе рейла.

Развилка для следующего захода: как показывать off-span слайсы — (a) edge-маркеры + рейл [рекомендация], (b) только edge-маркеры, (c) рейл с мини-панадаптерами.

Оговорки / риски

  • CPU софтовых слайсов ~линейно от их числа: каждый WDSP-канал жуёт полный rate и децимирует сам (аппаратный DDC дешевле — децимирует железо). Для QO-100 span мал, несколько слайсов — норма.
  • Не портить входной IQ-буфер между каналами (fexchange0 пишет in-place).
  • Pluto: слайсы только в пределах захваченной полосы; выход за край = ретюн LO (двигает всё, при QO-100 LO залочен на маяк — не трогать).
  • Пороговая аккуратность: старт/стоп WDSP-каналов на лету (см. заметку про задержки RXA стоп/старт в WDSPEngine.pas:~230).

Связанные заметки памяти

  • project_pluto_integration, project_qo100_zoom_panel, project_qo100_decoder_lock
  • project_spectrum_zoom (GetViewWindow / оверлеи / маппинг мыши)
  • project_channels_feature (dropdown/форма-редактор как паттерн UI)