Три связанных правки по отзывам с эфира.
1. Активный профиль однозначно определяется парой «TX-источник + его
модуляция»: ячейка таблицы заполнена — её профиль, пуста — базовый (первый в
списке). Прежняя «точка возврата» (запомнить, что было активно до автоматики)
ломалась в самом частом случае: заходя в FM, оператор обычно УЖЕ имел активным
профиль FM с прошлого захода, точка возврата записывала его же, и выход в USB
ничего не менял. Плюс она жила только в сессии и после перезапуска не работала
вовсе. Поле FTXProfPreAuto удалено.
Ручной выбор (дропдаун TX, список настроек, пилюля флага) дополнительно
ЗАПОМИНАЕТСЯ за текущей парой — это и есть обещанное «профиль помнится по виду
модуляции»: раньше в таблицу писала только пилюля, а выбор дропдауном следа не
оставлял. В DMR и FMRAW автоматика активный профиль не трогает вовсе.
2. Смена диапазона и вход/выход трансвертера меняют режим главного в
ApplyBandDSP (FMode := B.Mode) МИМО SetMode, поэтому автоматика там не
вызывалась и при переходе КВ↔трансвертер оставался профиль прошлого режима.
Зовём и оттуда. Заодно: FDSPEngine.SetMode насильно ставит FTXMode в режим
главного, а SetMode контроллера следом возвращает режим источника —
ApplyBandDSP этого не делала, и передача со слайса после смены диапазона
уезжала в чужую модуляцию.
3. Кнопка выбора TX-профиля гаснет, когда режим ИСТОЧНИКА передачи DMR или
FMRAW (в DMR передатчика нет, в FMRAW профиль обходится); открытый список
закрывается. Гейт по источнику, а не по главному: с главным в DMR можно
передавать со слайса. Смена режима на слайсе теперь тоже пересчитывает
доступность TX-контролов — это чинит и MOX/TUN/PS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CalcSliceBandX берёт кромки TX из FTXEdge* ТОЙ вьюхи, где живёт слайс, а
PushFilterEdgesToView отдавала их только FSpecView (пан 0). У панов N поля
оставались нулями, ветка TX не срабатывала, и слайс на передаче показывал
свою RX-полосу независимо от профиля и TX-фильтра.
Кромки TX уходят во вьюхи всех панов, кадр каждого помечается грязным.
RX-кромки главного туда не отдаём: на панах N главный VFO не рисуется.
Подтверждено на железе.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Одного значения на флаг мало: на слайсе с FT8 и на нём же голосом нужен разный
звук, а слайс/главный меняют модуляцию на лету. Привязка стала таблицей
TTXProfModeTable (режим → индекс профиля, -1 = не переключать), своя у каждого
флага: MainByMode у главного (одна на устройство, общая для КВ и трансвертера)
и TXProfByMode у слайса (сама разъезжается по контекстам — записи слайсов
хранятся отдельно для КВ и трансвертера).
Ключ — режим САМОГО ФЛАГА (FlagMode): у главного FMode, у слайса свой. Выбор в
пилюле пишется в ячейку текущего режима, поэтому смена модуляции достаёт
профиль, с которым в этом виде работали в прошлый раз.
Применение — в трёх точках, все ДО пуша режима в WDSP (ApplyTXChainSettings
внутри SetTXMode перешьёт цепь уже новыми значениями): SetTxSlice, SetMode
(если передаём с главного) и SetSliceMode (если источник — этот слайс).
Персист: tx_prof_modes в записи слайса, main_prof_modes в секции tx_profiles.
Чтение защитное — индекс вне текущего списка профилей читается как -1.
Round-trip проверен прогоном: обе таблицы переживают запись/чтение, незаданные
режимы -1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Передатчик один, профиль до сих пор был один на устройство: активный профиль
звучал независимо от того, какой слайс взял передачу. С панадаптером на FT8 и
вторым на FM это значит, что компрессия и EQ голосового профиля уходили в
эфир на FT8 (FMRAW прикрыт сам — ApplyTXModeSettings глушит там всю обработку,
а DIGU/DIGL нет). У Flex это решается дисциплиной и внешним API; делаем в радио.
Привязка «источник передачи → профиль» (-1 = не переключать, звучит активный):
TCtrlSlice.TXProfile для B..G, TTXProfileList.MainBind для главного (слайс A).
Персист: ключ tx_profile в слайсе пана и main_bind в секции tx_profiles.
SetTxSlice зовёт ApplyBoundTXProfile ДО пуша режима и цепи, иначе
ApplyTXSettingsToDSP лёг бы на старый режим. Привязали источник, который
передаёт прямо сейчас, — применяем сразу.
Привязки — индексы, поэтому RemapTXProfileBindings: удаление профиля из
середины снимает привязки на него и сдвигает те, что правее; Factory-сброс
снимает все (список заменён целиком).
UI: пилюля с именем профиля в первой строке флага сразу за шириной фильтра
(ЛКМ — fly-out список, первая строка «As active»). Правая граница считается
до SPLIT у главного и до RXM у слайса; не рисуем при пустом списке, в DMR
(передачи нет) и в FMRAW (профиль там ни на что не влияет). Прокрутка списка
общая с AUD — блок стрелок вынесен в DrawScrollArrows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ключ ко всему — семантика EQ в WDSP (wdsp/eq.c, eq_impulse + TXA.c):
F[1..n] это УЗЛЫ ломаной АЧХ, между ними интерполяция по децибелам, а за
крайними узлами при ctfmode=0 (так создаётся eqp у TXA, мы его не меняем)
идёт кумулятивный скат (f/f0)^4 на каждый бин — фактически обрыв. Значит
включённый EQ работает как второй полосовой фильтр, и верхний узел обязан
лежать за верхней кромкой TX-фильтра.
Settings: заводские ESSB несут свою кривую (AddEQ) — ESSB 3.5k узлы
100..3600, ESSB 5k узлы 100..5100, преамп в минус (EQ стоит до компрессора
и ALC). EQ у Default/SSB DX/SSB Wide сбрасывается в заводской ноль (FlatEQ),
а не наследует живой, иначе «сброс к заводским» не сбрасывал бы тембр.
Сетка узлов вынесена в модульную DEF_EQ_FREQS.
WDSPEngine: единая точка пуша EQ (PushTXEQProfile). Починен 3-полосный
режим: раньше в WDSP уходили первые три узла 10-полосной сетки (100/200/400)
и включённый EQ убивал весь голос выше 400 Гц; теперь legacy-раскладка
самого WDSP (150/400/1500/6000, как SetTXAGrphEQ).
EqualizerControl: кривая — точный порт eq_impulse вместо гауссовых горбов
(сверено с C численно, расхождение 0.0 дБ), рисуется без клампа ±15, чтобы
обрыв за крайними узлами был виден. В 3-полосном режиме ручки стоят на
150/1500/6000 и по частоте не таскаются, сетка профиля при этом цела.
RadioController/SettingsForm/MainForm: кнопка Factory — пересборка заводского
набора с применением Default. Без неё обновлённые заводские профили не доедут
до тех, у кого секция tx_profiles уже записана: она читается, а не создаётся.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кромки TX-полосы в SpectrumView обновляла только PushFilterEdgesToView,
которую звали из rfMOX/rfMode/rfFilter. Смена TX-профиля (rfTXProfile) и
правка Low/High во вкладке Transmit перешивали WDSP, но вьюха продолжала
рисовать старую полосу до следующего MOX или смены режима — на передаче
эти события не приходят вовсе.
Зовём PushFilterEdgesToView из RenderTXProfile и из OnTXSettingsChange
(кромки берутся из движка уже со знаком боковой, TXSignedEdges). Кадр
форсируем через MainForm.FSpectrumDirty — он же покрывает TX со слайса,
где FTXOverlay=False, а полоса слайса читает те же FTXEdgeLo/Hi.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Профиль — именованный снимок «как я звучу», переключаемый одним кликом
(идиома FlexRadio, состав по таблице TXProfile у Thetis).
Граница «профиль ↔ устройство»:
- в профиль: микрофонный вход (jack/boost/bias/PTT/Tip-Ring/Line-in gain),
MicGain, кромки TX-фильтра, компрессор/leveler/ALC/phase rotator, EQ,
AM carrier, Drive и Tune level;
- у устройства остаются ATT on TX, FIR/фаза/окно фильтра (это задержка
тракта, а не тембр), TUNFreq, FM/CTCSS, TX Display, TX Grid и все
параметры PureSignal — смена профиля не должна перерисовывать спектр
и ломать калибровку PS.
Кнопки «сохранить» нет: активный профиль ведётся за живым состоянием
(StoreActiveTXProfile из OnTXSettingsChange и SetDrive) — как и все
остальные настройки в EWSDR, применяемые мгновенно.
Settings.pas: TTXProfile/TTXProfileList, TXProfileCapture/TXProfileApply
(единственные две точки, знающие состав профиля), DefaultTXProfiles
(Default/SSB DX/SSB Wide/ESSB 3.5k/ESSB 5k — наследуют mic-вход и
мощность от текущих настроек), Load/SaveTXProfiles (секция tx_profiles
под MAC). Секции нет — заводской набор строится из текущих настроек,
поэтому миграция старого конфига не меняет звук.
RadioController: FTXProfiles, rfTXProfile, SelectTXProfile (снимок → DSP
+ перепосыл DUC Specific при смене mic-байта + SetDrive + персист),
Add/Rename/Delete (последний профиль не удаляется).
MainForm: кнопка профиля в свободных колонках ряда DUP/RX MUTE — блок TX
не вырос по высоте; ЛКМ — список, ПКМ — Settings→Transmit→Profile.
SettingsForm: страница Transmit разбита на подвкладки Profile / EQ /
Hardware / Display + полоса профиля сверху; свиток 2000 px вместо этого
раскладывается на четыре коротких экрана.
Проверено: round-trip персиста отдельным тестом (все поля, активный
индекс, усечение списка) и offscreen-снимки всех четырёх подвкладок.
На железе не проверено.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Фильтр главного приёмника перестал быть одним числом. Теперь это таблица
(Name, Low, High) на режим: 10 пресетов + VAR1/VAR2, знаковые кромки (у LSB
своя строка, у USB своя), паритет с Thetis FilterForm. Слайсы независимые
кромки умели и раньше — главный приёмник настраивался только пресетами.
Модель (Settings/RadioController):
- TFilterSlot/TFilterSet/TFilterTable + заводские дефолты из прежних FILT_*_BW
и FILT_*_NAMES (переехали в Settings — из них строится таблица).
- FFilterLo/FFilterHi = истина, FFilterBW производная (FilterBWFromEdges),
поэтому все прежние читатели ширины (web, зум, band-cache, CAT) не тронуты.
- ApplyFilterSlot — единственная точка «слот → состояние тракта».
- SetFilterEdges: произвольные кромки уходят в VAR1 с автопереключением,
пресеты нельзя испортить случайным движением мыши или энкодера.
- Персист: секция "filters" под MAC, пишутся только режимы, отличные от
заводских; секция режима сносится целиком, иначе возврат слота к
заводскому оставлял бы старый ключ.
- Диапазон помнит только выбранный слот: кромки живут в таблице, второго
источника истины у VAR нет.
UI:
- 12 кнопок фильтра (2 ряда по 6), подписи из живой таблицы.
- FilterPopup — поповер редактора по ПКМ на кнопке фильтра, по образцу
PureSignalPopup (дочерний контрол формы, не top-level: Wayland). Слева
слоты, справа Name/Low/High/Width, внизу форма полосы на знаковой оси.
- Перетаскивание края полосы на спектре: ±5px, снап 10 Гц, Alt = симметрично
(не Ctrl — он создаёт слайс). Отказ на передаче и когда полоса на экране
уже 12px, иначе захват съедал бы клик-тюн.
Отрисовка:
- Полоса главного VFO рисуется по РЕАЛЬНЫМ кромкам приёмника, слайса — по его
кромкам; вывод из режима и ширины остался фолбэком. Пара→ширина→пара было
преобразованием с потерями: несимметричный фильтр из слайс-CAT (ZZFL/ZZFH)
рисовался не там, где звучал.
- На передаче полоса считается по TX-фильтру (TXSignedEdges), а не по
приёмной ширине — TX-полоса от RX не зависит (модель Thetis).
- CalcFilterBandXFor больше не держит свою копию таблицы знака боковой:
единственная таблица теперь FilterEdgesFromBW.
Управление:
- CAT ZZFL/ZZFH заработали на основном порту (были заглушками).
- Андромеда: энкодеры #4/#5 — независимые Filter High/Low, как на железе G2
(было: #4 ширина, #5 не реализован).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Веб-пульт рисовал заметно грубее десктопа, и часть причин копилась давно.
Клиент вообще не разбирал бинарный фрейм 0x57: водопад рисовался из кадра
СПЕКТРА, а собственный поток водопада (свой детектор и усреднение
wf_avg_time_ms) уходил в никуда, занимая половину трафика. Теперь водопад
идёт из своего потока.
Адаптер слал константу 1024 вместо реального числа точек. Разрешение
анализатора = ширина панадаптера, и при узкой панели (грид из двух колонок,
маленькое окно) кадр короче — хвост буфера, нули = 0 dBm, уезжал клиенту
белой полосой, а частотная шкала сжималась. В демоне это обходили
DAEMON_SPEC_WIDTH. Длина фрейма теперь равна числу заполненных точек;
клиент и так считал N из byteLength.
Децимация под потолок протокола делила «i*Count div N» — на нецелом
отношении соседние группы получались по 1 и 2 точки, а смещение
max-детектора зависит от размера группы: по спектру шла гребёнка ±3 дБ,
в водопаде — вертикальная рябь. Заменено на целочисленный фактор
(группы одинаковой ширины).
Водопад терял 2 кадра из 3: анализатор даёт до 60 кадров/с, web шлёт 20
строк/с, а бралась просто «последняя». Введено двухступенчатое накопление
max-hold (DSP-поток → PushState → PushLoop) по идиоме
TWaterfallView.SetWaterfallData — короткие посылки CW/FT8 больше не
проваливаются между строками.
Отдельно исправлено в headless: PushState демона (20 Гц) и PushLoop (20 Гц)
идут вразнобой, и на дрейфе фаз одна и та же строка уезжала клиенту дважды
(водопад двоил и полз рывками), а на остановленном RX прокручивалась
застывшая строка. Фрейм 'W' теперь отправляется только при реально пришедшем
от DSP кадре (FWfAccumFresh → WfNew → FWfFresh). Спектр так не гейтится —
это живая кривая.
Рендер в браузере: значение бина для пикселя берётся максимумом по диапазону
вместо «ближайшего» (узкие сигналы мерцали и пропадали на узком экране), при
бинах реже пикселей — интерполяция; сглаживание спектра вынесено отдельным
проходом по всем бинам (в цикле по пикселям часть бинов не обновлялась
вовсе); строка водопада рисуется из сырых бинов, IIR ~200 мс остался только
для слежения за уровнями AGC — он размазывал водопад по вертикали.
Потолок точек кадра вынесен в настройку web.spec_pixels (Settings →
Advanced → Web Server → Spectrum: 1024/2048/4096, дефолт 1024). Буферы
кадра и WS-фреймы выросли под 4096 (WEB_SPEC_MAX, синхронно с
WDSPEngine.SPECTRUM_PIXELS). В демоне эта же настройка задаёт ширину
анализатора — рендера там нет, поэтому DAEMON_SPEC_WIDTH убран.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Вся раскладка окон, построенных кодом (SettingsForm, DeviceForm), была
захардкожена в пикселях под неявные 96 DPI — на Windows 125-200% или
HiDPI-десктопах контролы физически не росли вместе с укрупнившимся
шрифтом. Добавлено масштабирование SetBounds через MulDiv(V,
Screen.PixelsPerInch, 96) на каждой листовой точке потребления
(const-блоки раскладки не трогались).
Общий DpiScale вынесен в новый юнит DpiUtils.pas — убрана дублированная
копия одноимённого приватного метода в 9 классах (FlatCheckBox,
FlatComboBox, FlatListBox, FlatSpinEdit, FlatFloatSpinEdit, FlatEdit,
FlatRadioButton, FlatPopupMenu, SettingsForm, DeviceForm) и инлайн-MulDiv
без обёртки в MainForm.pas/PanafallPanel.pas.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Byte 1401 High Priority packet: per-band (HF 160m-6m) и per-slot (XVTR)
RX/TX-маски на 7 пинов + TX pin action (MOX/TUNE/2TON и комбинации).
Только openHPSDR — у Pluto/AD936x такого выхода нет, вкладка Settings
скрывается на этом бэкенде. Настройки — вкладка "OC Control" в SettingsForm.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
У каждого слайса B..G — свои Audio out / Audio in рядом с его CAT-портом.
Списки те же, что у главных комбо; пустой выбор (Default/None) = прежнее
поведение, общий выход/вход приложения.
Это устройство СЛОТА (буквы), а не шаблон:
• смена настройки применяется к слайсу, стоящему на слоте, немедленно
(SetSliceSlotAudio) — как и вся остальная форма настроек EWSDR;
• слайс, который встанет на слот позже (создание/restore из персиста),
получает то же устройство: слот сильнее аргумента AddSlice;
• выбор во вкладке AUD флага пишется обратно в настройку слота
(StoreSliceSlotAudio) — вкладка всегда показывает реальность, а не
вторую, конкурирующую истину.
Хранятся ИМЕНА PortAudio-устройств, а не индексы: порядок перечисления
между запусками плавает. Имя устройства, которого сейчас нет в системе,
не теряется — комбо добавляет его в список и держит выбранным.
ApplySliceSlotAudio зовётся на старте и на смене устройства (rfDevice):
слайсы восстанавливаются позже, слот должен знать свои звуковухи заранее.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Внешняя программа управляет отдельным слайсом как отдельным трансивером
через свой TCP-порт. COM-порты и 19090 остаются за главным приёмником
(слайс A) и не тронуты.
Протокол не дублируется: TCATEngine — чистый парсер поверх записи
callback'ов TCATContext, TCATTcpServer — транспорт поверх движка, поэтому
слайс-CAT = тот же движок с другим контекстом. Новый TCATSliceEndpoint
(движок + TCP-сервер на слот), массивом владеет TCATAdapter. Привязка к
СЛОТУ (буква B..G), не к Id: порт живёт, даже когда слайса нет (PS0).
Auto TX = «Auto Switch TX Slice» из SmartSDR CAT. Вся логика в одной точке
— TRadioController.RequestSliceTx, исполняется в потоке контроллера:
• пока кто-то уже в эфире (любой источник) — заявка игнорируется,
перехвата передачи нет никогда;
• без Auto TX слайс передаёт, только если уже выбран TX-источником;
• RX; снимает только СВОЮ передачу;
• TX-источник после отпускания остаётся на слайсе (как в SmartSDR).
На флаге слайса бейдж TX → AutoTX.
Частота слайса — TuneSliceInBand: свобода в пределах ВКЛЮЧЁННОГО диапазона
(трансвертер → его FreqBegin/FreqEnd, иначе band-план), другой диапазон —
отказ. Вне захваченной полосы окно DDC переезжает: доп. пан — центром на
цель (rfPanFreq → UI перекладывает флаги/шапку/зум-бар), пан 0 — центр
посередине между целью и главным VFO и только если оба влезают (общее
железное окно, главный приёмник не оглушаем).
Настройки: вкладка Slices (enable/порт 19091../Auto TX на слайс), персист
в секции cat + device-blob.
Попутно:
• ZZFL/ZZFH больше не заглушки — новые callback'и кромок фильтра
(у главного nil → прежний дефолт режима), хелпер SignedPad;
• FilterIdxFor, AGCModeToUI, BoardUtils.BandEdges;
• DSP-состояние слайса (NR/NB/SNB/ANF) зеркалится в TCtrlSlice — флаг
слайса рисовал нули и AGC главного;
• FUIReady в TMainForm.OnControllerState: адаптеры, создаваемые по ходу
FormCreate, больше не могут уронить старт рендером до постройки
виджетов (на этом падал Auto TX из настроек).
Проверено вживую: все порты 19091..19096 слушают, PS;/ZZFA;/MD;/ID;/SM0;/
ZZTX; отвечают; старт со всеми включёнными слайсами чист под gdb.
Документ — doc/CAT_SLICES.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Доп. паны не имеют своего FMode — режим берётся со слайсов пана:
PanHasFMSlice(PanId) = есть слайс в FM/DMR/FMRAW. По нему SyncPanZoomUI
выставляет View.FMGridStepHz = FM_STEP_HZ[FFMStepIdx], как SyncSpecViewFreq
делает для главного пана (сеттер сам гасит кэши спектра и линейки).
Тюнинг слайса привязан к той же сетке:
• колесо над слайсом/доп. паном — было зашито 12500 и только DMR/FMRAW,
теперь FM_STEP_HZ[FFMStepIdx] во всех трёх канальных режимах (гейт FFMStepOn);
• PanClickTune — там была та же зашитая 12500, теперь шаг режима целевого
слайса, как в DoSpectrumClick главного пана.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Доп. панадаптер приходит со своим слайсом и сразу рисует его флаг, а
главный приёмник оставался без флага — [A] показывался только когда слайс
создавали на самом пане 0 (инвариант «панель скрыта ИЛИ есть слайсы B+»).
TPanafallPanel: третье слагаемое инварианта — MainFlagForced, выставляется
хозяином. MainForm.ResizeSpectrumPanels после LayoutPanStack ставит
FPan.MainFlagForced := PanCount > 1 и зовёт UpdateMainFlagVisibility; это
единая точка — добавление/закрытие пана, restore персиста и STOP все идут
через пересчёт раскладки. Сворачивание левой панели (MainFlagPinned) на
это не влияет: пока жив хотя бы один доп. пан, флаг A остаётся.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Раньше «▦» имел смысл только при >=2 доп. панах (с одним грид совпадал со
стеком) — основной и единственный дополнительный приёмник могли стоять
лишь друг под другом. Теперь при ОДНОМ доп. пане грид = две колонки во всю
высоту: пан 0 слева, доп. справа, между ними тот же вертикальный сплиттер.
Wideband при этом остаётся сверху во всю ширину PanelRight, обе колонки
начинаются под ним (StackTop=AWideH, пан 0 раскладывается с ATopOffset=0,
шапки панов уходят в свои колонки — Pan0HeaderH над wideband не резервируется).
MainForm: ExtraPanCount/PanGridTwoCol; FPanColShare расширен на индекс 0
(пан 0 — тоже колонка), доля персистится через Cfg.Pans[0].ColShare — ключ
col_share в Settings уже был, теперь пишется для всех панов; отдельная ветка
в LayoutPanStack (горизонтальные сплиттеры спрятаны, кламп 15% ширины на
колонку); vsplit-хендлеры ищут левую колонку downto 0 в two-col и нормируют
доли с учётом пана 0; PanSplitMouseDown/Up гейтятся not PanGridTwoCol;
кнопка «▦» видна уже при одном доп. пане.
Отдельно исправлен sync-гард в тике дисплея: он сверял PbWideband.Width с
FPan.SpectrumWidth, а в two-col пан 0 — половина ширины при wideband во всю,
поэтому условие было вечно истинным и ResizeSpectrumPanels вызывался каждый
кадр — раскладка пересчитывалась под курсором (вертикальный сплиттер не
тянулся) и зря сбрасывались битмапы. Теперь wideband сверяется с
PanelRight.ClientWidth — его настоящим инвариантом.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VfoOverlay запоминает прямоугольник списка (FAudListRect) и обрабатывает
колесо над ним (HandleMouseWheel): шаг = одна строка. Если курсор в этой
зоне, событие считается обработанным и не уходит в спектр — частота не
съезжает даже когда список целиком помещается и крутить нечего.
PanafallPanel.DispatchFlagsMouseWheel перебирает главный флаг и флаги
слайсов (как остальные Dispatch*), MainForm.FormMouseWheel зовёт его по
всем панам первым делом после дисплеев VFO; координаты берутся через
PbSpectrum.ScreenToClient(Mouse.CursorPos), поэтому работает и в pop-out.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Паритет с уже существующим выбором вывода: на каждом слайсе своё устройство
входа (микрофон/виртуальный кабель).
- RadioController: TCtrlSlice += InDeviceIndex/InDevName/AudioIn (ссылка, не
владение); mic-пул FMicPool с refcount (Acquire/ReleaseMicInput) — один
открытый TAudioInput на уникальное PA-устройство, смена TX-слайса не
переоткрывает поток; ActiveMicInput/ActiveMicInDevName для PullSoundCardMic,
Flush и DefaultMicSource; SetMainMicDevice перепривязывает ссылки слайсов.
- VfoOverlay: во fly-out AUD ряд вкладок OUT | IN, событие OnAudioInDevice.
- PanafallPanel: проброс списка входных устройств во флаги слайсов и главный.
- Settings: персист in_dev_name.
НЕ ГОТОВО: зависание GUI при выборе на слайсе того же входа, что в настройках
(пул открывает поток вместо шортката на общий FAudioIn). В коде оставлена
временная диагностика [MIC] в stderr. Также подозрение на замедление потока
данных от радио — проверяем сравнением с main.
Со мной в коммите временные тестовые программы PortAudio (other/pa*.lpr).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
На Pluto шкала dBm ничего не значила: усиление тракта 0..73 дБ входит в
показания напрямую, а в режимах AGC железо крутит его само, и картинка
«дышит» вместе с ним.
- RadioBackend: виртуальный RxGainNowDb (TELEMETRY_NONE = регулируемого
усиления нет, как у openHPSDR с его аттенюатором).
- PlutoBackend: в MANUAL отдаёт FGainDb (точно, без опроса железа), в режимах
AGC — hardwaregain из телеметрического потока (читается там же, где rssi,
~2 Гц, не блокируя UI).
- RadioController: AttenCalDB -> FrontEndCalDB, общее понятие «регулируемое
усиление входа»: openHPSDR +FAtten (аттенюатор режет), AD936x -hardwaregain
(усиление поднимает). Постоянная часть тракта (LNB, кабель, конвертер)
остаётся за таблицами калибровки на диапазон/трансвертер.
- UI: в блоке RF AGC при режиме != MANUAL вместо надписи «AGC» — фактическое
усиление («~38dB», тильда = измеренное), ползунок ездит сам. Видно, что
цифра гуляет, а уровни на спектре стоят.
Ограничения: FAST AGC крутит усиление быстрее опроса (2 Гц), для стабильной
шкалы нужен MANUAL или SLOW; шумовая полка при смене усиления немного едет —
это физика (input-referred NF), компенсацию проверять по сигналу.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Аттенюатор был только в web и только тремя ступенями (0/-10/-20), а его дБ
никуда не возвращались — при ATT 20 дБ S-метр и спектр уезжали на 20 дБ, и
свежая калибровка уровней была верна лишь при ATT=0.
- RadioController: SetAtten(Db) 0..31 (как умеет железо) вместо индекса *10.
AttenCalDB входит в SMeterCalFor и DispCalFor — стрелка, спектр, водопад и
паны компенсируются, шкала dBm остаётся абсолютной (как Thetis/piHPSDR).
У AD936x аттенюатора нет, там поправка 0 (усиление тракта — hardwaregain).
- Память на диапазон/слот трансвертера по образцу RxGainMode/RxGainDb:
TBandSettings.AttenDB (band 'atten_db') и TXvtrEntry.LastAttenDB
('last_atten_db'); восстановление и отправка в железо — в ApplyBandDSP,
сбор — в MakeBandSettings/SaveCurrentBand, вход в слот — через B.AttenDB.
SetAtten сразу кладёт значение в кэш диапазона/слота (JSON пишется общим
сохранением, а не на каждый шаг ползунка — как у drive).
- MainForm: ряд «ATT [ползунок] NNdB» в RX-блоке левой панели, отступы как у
VOL; на Pluto ряд скрыт и RX-блок ужимается (LayoutLeftPanel двигает
панели ниже). Внешние изменения приходят по rfAtten.
- Web: select → ползунок 0..31 с цифрой, attn_idx -> attn_db, cmd attn {db:}.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Вместо одного офсета на всё устройство — таблицы «на диапазон» и «на слот
трансвертера»; состав таблиц задаёт активное железо.
- Settings: SMeterOffsetDB -> SMeterBandDB[12] / SMeterXvtrDB[8] (JSON
cal_smeter_band_i / cal_smeter_xvtr_i). Легаси-ключ cal_smeter_db становится
дефолтом всех слотов — показания после обновления не меняются. Разделение
Pluto/openHPSDR даёт сама секция JSON: она ключуется по MAC устройства
(у Pluto — synthetic MAC из serial).
- Офсет убран из WDSPEngine (FSMeterCalDB): движок не знает ни диапазонов, ни
трансвертеров. Теперь TRadioController.SMeterCalFor(VisibleHz) — окно
включённого XVTR-слота приоритетнее диапазона, индекс диапазона по плану
железа (FreqToBandIdx с IsPluto); ReadSMeterDBm для главного S-метра
(MainForm + ewsdrd) и SliceSMeter по TargetHz слайса — паны N на других
диапазонах считаются по своему банду, а не по главному.
- SettingsForm: таблицы на 12 слотов, подписи/видимость/раскладку ставит
RelayoutCalTab; SetCalibrationBackend(IsPluto) вызывается рядом с
SetAntennaBackend. У openHPSDR — HF-план, у Pluto — VHF/UHF-план и скрытые
группы Power/SWR detector и Supply V/A (нет Alex-моста и его телеметрии).
Слоты трансвертеров показываются только включённые, с их именами.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The RX timeout check sampled GetTickCount64 on the UI thread while the UDP receive thread could update FRXLastPktTime a millisecond later. Both values are QWord, so subtracting the newer packet tick from the older UI tick underflowed to a very large value and falsely satisfied the 3-second timeout.
Snapshot the packet timestamp once for each timeout check and require NowTick >= LastPktTick before calculating the elapsed interval. The initial-packet timeout uses the same snapshot. This preserves real timeout detection while ignoring a packet timestamp that is marginally newer than the UI tick.
Diagnostics confirmed that DDC UDP packets, callbacks, and HP status remained continuous during the reported failures; the issue was local timeout arithmetic rather than network loss or radio stream interruption.
Mouse wheel over an extra panadapter now retunes that pan's primary
slice (first created on that receiver) instead of the main VFO; a slice
flag under the cursor still wins, pan 0 keeps driving the VFO.
Slice flags get a clickable RXM badge: RX audio of the slice is muted
while transmitting (default), so the operator no longer hears himself on
every slice in full duplex. State persists with the workspace.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Клик по «NNN kHz · DDCn» в шапке пана — кроме сетки rate теперь есть пункты
«→ DDC N»: пан переезжает на другой аппаратный DDC (MovePanDDC), при этом
движок, анализатор и слайсы не пересоздаются — перепрошивается только
hw-индекс. Пригодилось при отладке: конкретный DDC платы может вести себя
хуже соседей, и пан можно просто увести на другой.
Диапазон ограничен реальными DDC железа: MaxDDCIndex = 7 (DDC8+ openHPSDR
enable-бит принимает, но поток не отдаёт). Ограничение действует и в меню,
и в DDCFree, и в самом MovePanDDC.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Второй разъём у AD936x-плат — это второй КАНАЛ чипа (не порт B первого), и в
1R1T-образе он выбирается мультиплексором AD9361: debug-параметры
adi,1rx-1tx-mode-use-rx-num/-tx-num + биты RX1/RX2, TX1/TX2 регистров 0x3/0x2
(как switch_rfinput.sh/switch_rfoutput.sh прошивки). rf_port_select тут ни при
чём — прошивка его к тому же блокирует.
- IIOBindings: debug-атрибуты + прямой доступ к регистрам.
- PlutoBackend: детект второго разъёма (2R2T-каналы либо мультиплексор + модель
платы), SetAntPorts(RxPort,TxPort), ApplyRxMux/ApplyTxMux; Caps.HasAntSwitch.
⚠ApplyGain(Force): драйвер пропускает запись gain_control_mode, если считает
режим неизменным, — а у нового канала AGC-регистры не запрограммированы (в
железе крутится auto-AGC, hardwaregain отвергается EOPNOTSUPP). Форсируем
перепрог через другой режим после смены разъёма и после ad9361_set_bb_rate.
- RadioBackend: TBackendCaps.HasAntSwitch + virtual SetAntPorts (HPSDR — no-op,
там антеннами рулит Alex).
- Settings: секция ant936x (разъём RX/TX на диапазон и на слот трансвертера);
hw-AGC (режим + manual gain) переехал в TBandSettings/TXvtrEntry — теперь он
свой на каждом диапазоне и трансвертере.
- RadioController: FAnt936x + ApplyAntPorts (RestoreBand/ActivateXvtr/
StartRunning); ApplyBandDSP восстанавливает hw-AGC диапазона.
- SettingsForm: вкладка Antenna — Alex-таблица (openHPSDR) либо таблица
разъёмов AD936x по диапазонам/трансвертерам.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- MainForm: TXOverlay главной вьюхи взводился по FTransmitting без учёта
источника TX → красной становилась полоса главного VFO даже при передаче
со слайса. Теперь TXOverlay = FTransmitting and (FTxSliceId = 0).
- VfoOverlay: публичный TxActive (FTx) — вьюхи знают, какой слайс передаёт.
- SpectrumView/SpectrumViewOpengl: полоса/края/несущая передающего слайса
красные (alpha 100, как у главного при TX), иначе цвет бейджа слайса.
- Фикс цвета TX-полосы в GL: $00E03030 при ColorToRGB (младший байт = R)
давал синий вместо красного; общая константа CLR_TX_BAND = $003030E0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
У слайса нет своего VFO B — кросс-частотный TX делается TX-бейджем слайса.
Бейдж SPLIT (и его хит-зона) рисуется лишь при IsMain; «×» флага слайса
переехал вплотную к TX. SPLIT главного гаснет, пока TX-источник — слайс
(ActiveTXFreqHz берёт частоту слайса, а не VFO B).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- RadioController: FTxSliceId (0=главный VFO, >0=слайс), SetTxSlice
эксклюзивно; ActiveTXFreqHz/ActiveTXMode с TX-слайса, режим применяется
в SetMOX(True) перед SetTXRun; TxBandIdx для PA-кал и safety DoNotTx
при кросс-банде; RemoveSlice сбрасывает TX-слайс.
- HPSDRNetwork: TX-антенна выбиралась по RX-бэнду → по TX-частоте
(TxAnt[FreqToBandIndex(TXFreqHz)]).
- MainForm/PanafallPanel: клик TX на флаге слайса → SetTxSlice, TX-бейджи
(янтарь=выбран, красный=передаёт), RefreshTxIndicators по rfTransmitting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Селекторы диапазона/samplerate панов N переведены с нативного TPopupMenu на
своё меню в стиле приложения (одинаково на всех платформах, в цветовой теме).
- FlatPopupMenu.pas (new): TFlatPopupMenu — owner-draw меню как ДОЧЕРНИЙ контрол
формы (не top-level окно: Wayland центрирует свои top-level окна). Hover,
галка текущего, клавиатура (↑↓/Enter/Esc), закрытие по клику-вне (MouseCapture)
и потере фокуса. Позиционируется под лейблом-якорем в клиентских координатах.
- PanDisplayPopup: поповер «◑» тоже переведён с top-level формы на дочерний
контрол (центрировался на Wayland) — открывается под кнопкой, закрытие «×»
или повторным «◑».
- MainForm: band/rate-селекторы на FlatPopupMenu; свёртка повторяющегося
выбора темы (9 инлайнов) в CurrentAppTheme.
- PanafallPanel: курсор-«палец» кликабельных лейблов шапки переприменяется в
конце Build (флаги *Clickable могли выставляться до создания лейблов —
samplerate-лейбл не получал crHandPoint).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Кликабельный бейдж диапазона («20m») в шапке пана N → popup-меню КВ-бандов.
На выбор — по-флексовому: ретюн DDC на VfoA диапазона (из общего band-кэша,
как пан 0), активный приёмник садится на VfoA с mode/filter/AGC + zoom/pan
этого диапазона, прочие слайсы пана убираются (вне новой полосы). Только на
настоящих КВ (не XVTR, не Pluto).
- TPanafallPanel: FHeaderBandLabel + OnBandClick/BandClickable/SetHeaderBand;
каскад left-лейблов шапки вынесен в LayoutHeaderLabels (band/rate/adc без
дыр при скрытии).
- MainForm: OnPanBandClick/PanBandMenuItemClick/ApplyPanBand +
PanBandSelectorVisible (гейт КВ); band-лейбл в UpdatePanHeaders (по
FreqToBandIdx частоты DDC), тема в ApplyPanHeaderTheme.
Ограничение: per-slice AGC-Top в движке нет — подтягиваются
mode/filter/AGC-mode/zoom/pan, но не индивидуальный AGC-Top диапазона.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Сохранение и восстановление панадаптеров, слайсов и раскладки per-device,
раздельно по контексту: КВ ('hf') и каждый трансвертер ('xvtr_N') — паны
одного контекста не имеют смысла в другом.
Settings: секция "pans"/<ctx> под MAC (grid-режим/доли, на пан —
freq/rate/adc/zoom/pan/доли стека+колонки+сплиттер спектр/водопад, на слайс —
freq/mode/фильтр/agc/vol/mute/squelch/аудиоустройство). TSliceCfg/TPanCfg/
TPansConfig + Default/Save/LoadPans(Ctx).
MainForm: CreatePanCore (вынос ядра из OnAddPanClick), SavePanWorkspaceCtx/
RestorePanWorkspaceCtx/TearDownWorkspace, PanContextKey. Хук контроллера
OnBeforeStop (снимок до гашения DDC в StopRunning). Переходы ActivateXvtrBand/
DeactivateXvtr обёрнуты save-старого/teardown/restore-нового (гейт FRunning);
restore XVTR-панов после ActivateXvtr (FCurrentXvtr целевой ⇒ XvtrTranslate
верный). PanafallPanel.DestroyAllSliceFlags. Смена КВ-банда контекст не меняет.
Round-trip сериализации и разделения контекстов проверены автономно; на
экране не проверено.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Частота пана хранится ВИДИМОЙ (как FVfoA/FCenterFreq) и транслируется в
IF через XvtrTranslate только на границе с железом (AddPanDDC,
SetPanDDCFreq, SetPanDDCRate, SetPanADC) — раньше DDC пана получал
видимую частоту сырьём, а drag водопада пана N клампился пределами АЦП
(~61 МГц) вместо диапазона трансвертера.
Новое: controller.VisibleFreqBounds (диапазон активного XVTR | пределы
железа) для UI-клампа драга; RefreshPanDDCs — на входе/выходе XVTR и
правке LO/LOError клампит видимые центры живых панов, перешивает
freq-слова DDC и прижимает выпавшие слайсы к краям полосы захвата.
Подтверждено юзером на трансвертере.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>