Commit Graph
461 Commits
Author SHA1 Message Date
ew8bak 19babef4d7 ui(status): use system font 2026-08-05 16:11:57 +03:00
ew8bak a0f4b044db ui(spectrum): theme sample rate overlay 2026-08-05 16:07:25 +03:00
ew8bak b70daa9dbf ui(main): fix DPI layout and theme switching 2026-08-05 15:55:03 +03:00
ew8bak 998cb3f0fe ui(beacon): add flat memo with overlay scroll 2026-08-05 15:06:48 +03:00
ew8bak 0396eeed49 ui(channels): redesign editor and fix DPI scaling 2026-08-05 14:50:15 +03:00
ew8bak 5120d8d21d ui(beacon): redesign decoder diagnostics form 2026-08-05 14:35:39 +03:00
ew8bak 960f221179 ui(settings): replace native vertical scrollbar 2026-08-05 14:10:20 +03:00
ew8bak 1d04973d6f fix(discovery): use system fonts and correct DPI scaling 2026-08-05 13:49:23 +03:00
ew8bak b7b9119339 feat(ui): add hover overlay scroll for left panel 2026-08-05 13:44:12 +03:00
ew8bak 94bb15bbaf fix(ui): scale Pluto left-panel relayout 2026-08-05 13:24:54 +03:00
ew8bak 907c2c05c3 ui: use system font in flat controls 2026-08-05 13:20:03 +03:00
ew8bak bd057a7849 fix(settings): prevent duplicate DPI scaling 2026-08-05 12:42:56 +03:00
ew8bak 9047dc586d ui(settings): align cards and spacing 2026-08-04 21:02:15 +03:00
ew8bak adf5c5016e ui(settings): refine transmit and hardware layouts 2026-08-04 20:47:55 +03:00
ew8bak 612bc73687 ui(settings): add responsive page layout 2026-08-04 20:36:17 +03:00
ew8bak 304e574592 ui(settings): normalize layout and system fonts 2026-08-04 20:23:33 +03:00
ew8bakandClaude Opus 5 4e6703e590 fix(xvtr): TUN идёт от глобального уровня, мощность слота — множитель
При включённой опции TUN в трансвертере брал мощность слота ВМЕСТО глобального
TUN Level, из-за чего ручка «Tune level» на вкладке Transmit в трансвертере
была мёртвой, а настройка уходила в эфир на мощности слота (у 70cm и QO-100 —
все 100 %).

Теперь уровень всегда от TUN Level, а слотовая TX Power работает множителем:
Pos = TUNLevel * слот/100 — та же формула, по которой в трансвертере уже
масштабировался обычный drive. Смысл поля становится единым: «сколько мощности
трансивера разрешено этому трансвертеру», и к настройке это относится так же,
как к передаче.

Галка сохранена (кому нужен неотмасштабированный TUN), но переименована в
«Scale TUN by XVTR power» — прежнее название описывало снятое поведение.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 19:40:59 +03:00
ew8bakandClaude Opus 5 199d0ffa85 ui(tx): треугольник пилюли профиля выровнен по надписи
Треугольник центрировался по прямоугольнику пилюли, а текст — по TextHeight,
который включает выносные элементы вниз. В подписи одни заглавные, они сидят
в верхней части бокса, поэтому треугольник визуально проваливался: по пикселям
текст занимал строки 7..13, треугольник 11..15.

Считаем от метрики самой надписи (TxtY + TextHeight div 2 - 2), а не от бокса,
чтобы выравнивание пережило смену шрифта или кегля. После правки текст 7..13,
треугольник 8..12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 19:30:54 +03:00
ew8bakandClaude Opus 5 249881a579 fix(tx): профиль по модуляции — детерминированное правило + пробелы вызова
Три связанных правки по отзывам с эфира.

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>
2026-08-04 19:23:49 +03:00
ew8bakandClaude Opus 5 82df4dfb8c feat(tx): заводской профиль FM — плотная модуляция с компрессией
Шестой заводской профиль: компрессор 8 дБ + своя кривая EQ. Для FM компрессия
работает иначе, чем для SSB: девиация ограничена жёстко, громкость даёт
средний уровень, а не пики. По цепи TXA (wdsp/TXA.c:xtxa) eqp → leveler → bp0
→ compressor → ALC идут ДО xfmmod, то есть профиль на FM-звук влияет целиком.

Кривая намеренно не задирает верх: предыскажение (xemphp) по умолчанию стоит
вторым вариантом — после ALC, прямо перед модулятором, — поэтому компрессор
его не удержит и лишний буст ушёл бы в перемодуляцию на свистящих. Снизу срез
бубнения, лёгкий подъём разборчивости 1.4..2.3 кГц, спад к 3.4 кГц. Крайние
узлы 200 и 3400 взяты с запасом вокруг AF-полосы модулятора 300..3000, чтобы
обрыв EQ не попал внутрь неё (проверено расчётом по формуле eq.c).

Полоса 300/3000 в профиле для самого FM косметическая — края bp0 движок
считает по Карсону; она сыграет, только если привязать профиль к голосовому
режиму. Девиация, AF-срез и позиция предыскажения остаются свойствами
аппарата (вкладка Hardware) и профилем не переключаются.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:47:22 +03:00
ew8bakandClaude Opus 5 0ba808e886 fix(tx): Factory возвращает заводской звук, а не снимок живых настроек
«Default» строился через TXProfileCapture(живые настройки), поэтому сброс,
сделанный на профиле ESSB 5k, оставлял в нём High cut 5000 — то есть Factory
не сбрасывал ровно то, ради чего его жмут. Правило «заводское значит
заводское» было применено только к EQ (FlatEQ), теперь ко всему тембру.

База заводских профилей — DefaultTX, поверх копируется только живая обвязка
микрофона: разъём и режим (Mic In/Line In, boost, bias, PTT, Tip/Ring),
Line In gain и Mic gain. Их сброс сорвал бы настройку микшера. Мощность тоже
остаётся живой — скачок drive ушёл бы в усилитель. Всё остальное (полоса,
компрессор, leveler, ALC, phase rotator, EQ, AM carrier, TUN level) заводское.

Текст подтверждения и подсказка обещали, что полоса и динамика останутся как
есть, — переписаны под новое поведение.

Проверено прогоном: с живыми ESSB 5k + компрессор 12 дБ + Line In + mic gain
22.5 заводской Default выходит 200/3100, comp off, EQ ровный, а Line In,
mic gain и drive сохраняются.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:34:36 +03:00
ew8bakandClaude Opus 5 f320ef1bcf fix(tx): полоса передающего слайса на доп. панах рисовалась по RX-ширине
CalcSliceBandX берёт кромки TX из FTXEdge* ТОЙ вьюхи, где живёт слайс, а
PushFilterEdgesToView отдавала их только FSpecView (пан 0). У панов N поля
оставались нулями, ветка TX не срабатывала, и слайс на передаче показывал
свою RX-полосу независимо от профиля и TX-фильтра.

Кромки TX уходят во вьюхи всех панов, кадр каждого помечается грязным.
RX-кромки главного туда не отдаём: на панах N главный VFO не рисуется.

Подтверждено на железе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:20:52 +03:00
ew8bakandClaude Opus 5 8f95795bbb ui(tx): вкладка Transmit целиком на английском + сетка группы Microphone
Страница была единственной с русскими подписями — переведены подсказки всех
четырёх подвкладок, надпись «Profile drive: N %» и диалоги профиля (New /
Rename / Delete / Factory / лимит). Строковых литералов с кириллицей в
SettingsForm больше нет.

Группа Microphone приведена к сетке страницы: подписи в колонке PAD, поля в
колонке CX (комбобокс Mic jack и спин Mic gain теперь в одной колонке),
Connector переехал во вторую пару колонок к Mic gain — на не-Orion он скрыт и
дыры не оставляет, поэтому отдельная третья строка не нужна (176 → 134 px).
Тумблеры boost/bias/PTT расставлены с равным шагом; пара «Line In gain»
занимает место чекбокса boost и заканчивается до «Mic bias».

Английские подписи длиннее русских — все проверены рендером формы offscreen,
в группы вписываются.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:08:59 +03:00
ew8bakandClaude Opus 5 36af031767 feat(tx): привязка профиля к паре «флаг + модуляция»
Одного значения на флаг мало: на слайсе с 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>
2026-08-04 17:58:46 +03:00
ew8bakandClaude Opus 5 013d0406ac feat(tx): TX-профиль на слайс — пилюля в шапке флага
Передатчик один, профиль до сих пор был один на устройство: активный профиль
звучал независимо от того, какой слайс взял передачу. С панадаптером на 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>
2026-08-04 17:06:08 +03:00
ew8bakandClaude Opus 5 8563c4e4cc feat(tx): заводские EQ-кривые ESSB, честная АЧХ в редакторе, фикс 3-полосного EQ
Ключ ко всему — семантика 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>
2026-08-04 15:47:12 +03:00
ew8bakandClaude Opus 5 c8781bee6a fix(tx): полоска TX-фильтра перерисовывается при смене профиля и правке Low/High
Кромки 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>
2026-08-04 14:50:06 +03:00
ew8bakandClaude Opus 5 71a97b63f0 feat(tx): TX-профили + вкладка Transmit на подвкладках
Профиль — именованный снимок «как я звучу», переключаемый одним кликом
(идиома 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>
2026-08-04 14:21:09 +03:00
ew8bak 24890ca8a7 Merge feature/filter-edges: произвольные кромки фильтра, редактор, drag, TX-полоска
Фильтр главного приёмника стал таблицей (Name, Low, High) на режим: 10
пресетов + VAR1/VAR2, знаковые кромки. Поповер-редактор по ПКМ на кнопке
фильтра, перетаскивание края полосы на спектре (Alt = симметрично), полоса
при передаче рисуется по TX-фильтру, CAT ZZFL/ZZFH на главном порту,
энкодеры #4/#5 Андромеды крутят High/Low раздельно.

Попутно устранён дубль таблицы знака боковой (RX-конвенция жила и в
контроллере, и в отрисовке) и потеря информации при рисовании полосы слайса
(пара кромок схлопывалась в одну ширину).

Проверено на железе: 25521a2 + фикс ce43116 (слот VAR терялся при рестарте
в режиме трансвертера).
2026-08-04 13:35:57 +03:00
ew8bakandClaude Opus 5 ce43116490 fix(filter): слот VAR переживает перезапуск в режиме трансвертера
Симптом: в XVTR-режиме выбрать VAR1, выключить и включить трансивер — фильтр
откатывался на заводской (в FM это NFM). Вне трансвертера всё сохранялось.

Причина: персист XVTR клампил last_filt диапазоном 0..9, оставшимся от эпохи
десяти пресетов. VAR1/VAR2 (10/11) при загрузке схлопывались в 9, а дальше
ClampFilterIdx видел индекс вне таблицы режима (у FM пресетов всего два) и по
своему правилу откатывался на заводской индекс — отсюда NFM. У band-cache
такой границы нет, поэтому в обычном режиме баг не проявлялся.

Граница поднята до FILT_SLOTS-1. Проверено round-trip-тестом SaveXvtr/LoadXvtr:
слот 10 сохраняется и читается как 10.

Проверены заодно остальные пути, где слот мог потеряться: band-cache (клэмпа
нет), каналы памяти (фильтр не хранят вовсе), рендер rfMode (сброс обёрнут
save/restore), web SetFilterBW, SanitizeFilterForMode. Клэмп 0..9 в CAT-командах
FW/SH/ZZBI намеренно оставлен — это ограничение протокола, а не хранения; VAR
снаружи доступен через ZZFL/ZZFH.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:22:56 +03:00
ew8bakandClaude Opus 5 25521a2d10 feat(filter): произвольные кромки фильтра — таблица по режиму, редактор, drag, TX-полоска
Фильтр главного приёмника перестал быть одним числом. Теперь это таблица
(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>
2026-08-04 13:15:33 +03:00
ew8bakandClaude Opus 5 a820182716 fix(web): детализация спектра/водопада — честный размер кадра, поток 'W', настройка разрешения
Веб-пульт рисовал заметно грубее десктопа, и часть причин копилась давно.

Клиент вообще не разбирал бинарный фрейм 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>
2026-07-31 15:35:55 +03:00
ew8bakandClaude Sonnet 5 c95620fb23 fix(ui): DPI-масштабирование SettingsForm/DeviceForm + общий DpiScale
Вся раскладка окон, построенных кодом (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>
2026-07-29 16:05:39 +03:00
ew8bakandClaude Sonnet 5 041bc6ba7a feat(oc): OC Control — Open Collector выходы Penny/Alex (эталон Thetis)
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>
2026-07-29 13:47:04 +03:00
ew8bak b6d2256416 Merge feature/slice-cat: CAT-порт на слайс + Auto TX, аудио in/out на слот слайса 2026-07-29 12:31:02 +03:00
ew8bakandClaude Opus 5 20186ce132 feat(slices): аудио-устройства слайса на вкладке Slices (in/out на слот)
У каждого слайса 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>
2026-07-25 14:33:42 +03:00
ew8bakandClaude Opus 5 2d15da0462 feat(cat): CAT-порт на каждый слайс + Auto TX (эталон SmartSDR CAT)
Внешняя программа управляет отдельным слайсом как отдельным трансивером
через свой 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>
2026-07-25 13:56:36 +03:00
ew8bakandClaude Opus 4.8 8611898ad1 fix(tx): WFM/FMRAW bp0 = аудио-полоса режима, не SSB-фолбэк
TXSignedEdges для WFM падал в else→сырую магнитуду (200,3100) и душил
вещательное аудио до телефонной полосы ещё ДО ЧМ-модулятора (у того свой
AF 20–15000). Добавлены ветки: WFM→(20,15000)=RX AF WFM, FMRAW→(20,7000)=
RX AF raw. Теперь TX-bp0 совпадает с RX-АУДИО-фильтром (истинная симметрия),
а не с РЧ-шириной канала FILT_WFM_BW/FILT_RAW_BW (это селективность RX,
другая величина). Развязка TX от RX для SSB/AM/CW не тронута.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:40:17 +03:00
ew8bakandClaude Opus 4.8 f425071461 fix(tx): ширина TX-полосы независима от RX-фильтра (модель Thetis)
Убрано RX→TX зеркало: ApplyModeFilter больше не тащит края RX-фильтра в TX
(FDSPEngine.SetTXFilter удалён и из вызова, и как метод движка — был
единственный вызов). Раньше TX bp0 писали два источника с разной шириной:
ApplyModeFilter (RX-ширина FFilterBW) и ApplyTXChainSettings/SetTXFilterFull
(TX-фильтр 200/3100) — на MOX побеждал TX-фильтр, между оверами держалась
RX-ширина, ширина прыгала.

Теперь TX-полосу задаёт ТОЛЬКО TX-фильтр из настроек (через TXSignedEdges),
независимо от RX. RX LSB 5.0k больше не раздувает излучаемый сигнал. База под
ESSB: расширение TX-фильтра в Setup сразу даёт нужную полосу передачи.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:28:01 +03:00
ew8bakandClaude Opus 4.8 51e8fc4782 fix(tx): боковая по режиму на всех путях + единая оркестрация TX/RX (эталон Thetis)
Боковая TX теперь строится ЕДИНЫМ конвертером знака TXSignedEdges (порт
Thetis UpdateTXLowHighFilterForMode): магнитуда TX-полосы → знаковые края bp0
по боковой режима. Через него идут ApplyTXChainSettings и SetTXFilterFull —
LSB/DIGL больше не уходят в USB, когда пуш оказывается последним (MOX).

Убраны оба SetTXABandpassRun(1): этот сеттер дёргает bp1 (aux), а не bp0.
При выкл. компрессоре bp1 держал стартовую полосу (−5000,−100) и глушил вход
модулятора — FM/WFM/FMRAW не заводились без «тычка» компрессором. bp1 отдан
компрессору через TXASetupBPFilters, как в Thetis.

Оркестрация сведена: ApplyTXMode (SetTXAMode+ApplyTXModeSettings) — единая
точка TX-перехода для Open/SetMode/SetTXMode (в т.ч. слайс-TX);
ApplyRXModeSettings (WFM/DMR/FMRAW) — для главного RXA и слайсов. Убраны
ручной DMR-инлайн в SetMode и двойной вызов WFM; ApplyWFMSettings→ApplyTXWFMSettings.

Дефолт TX-полосы 100/2800 → 200/3100 (Thetis-паритет).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:42:06 +03:00
ew8bakandClaude Opus 4.8 b7e277e72b Merge feature/pan-grid-two-col: грид панов из двух колонок + FM-сетка на доп. панадаптерах
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 19:22:49 +03:00
ew8bakandClaude Opus 4.8 02288292ff feat(pans): сетка FM-каналов и шаг тюнинга на доп. панадаптерах
Доп. паны не имеют своего 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>
2026-07-22 19:22:30 +03:00
ew8bakandClaude Opus 4.8 c857aa9d4b fix(slices): squelch слайса гейтится режимом — уход из FM больше не глушит
Юзер: на слайсе включил FM с шумодавом, переключил модуляцию — звука нет,
а шумодава в не-FM режимах нет, выключить нечем.

Блок fmsq WDSP стоит в тракте и глушит звук по шумовому выходу FM-
дискриминатора. У главного приёмника он всегда гейтится режимом
(ApplyModeFilter: SetFMSquelch((FMode = MODE_FM) and FFMSQOn, …), в не-FM
ветке — явное выключение), а у слайсов SetSliceFMSquelch дёргал
SetRXAFMSQRun(Chan,1) безусловно и после ухода из FM squelch оставался.

WDSPEngine: новый ApplySliceFMSQ(Idx) — FMSQOn and (Mode = MODE_FM), то же
правило, что у главного. Зовётся из SetSliceMode (ветка без пересоздания
канала) и SetSliceFMSquelch, тем же гейтом накрыт OpenSliceChannel
(пересоздание канала при смене rate/WFM). Выбор юзера не теряется: FMSQOn
остаётся в записи слайса и в персисте, при возврате в FM восстанавливается.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 12:16:49 +03:00
ew8bakandClaude Opus 4.8 3bdcfa0674 ui(pans): флаг A появляется вместе с первым доп. панадаптером
Доп. панадаптер приходит со своим слайсом и сразу рисует его флаг, а
главный приёмник оставался без флага — [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>
2026-07-22 12:11:57 +03:00
ew8bakandClaude Opus 4.8 6c38777c58 ui(pans): грид из двух колонок — пан 0 и доп. пан бок о бок
Раньше «▦» имел смысл только при >=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>
2026-07-22 12:08:06 +03:00
ew8bakandClaude Opus 4.8 ef444e6607 Merge feature/slice-audio-input: устройство ВВОДА на каждом слайсе
Вкладки OUT/IN в fly-out AUD флага, mic-пул с refcount (переключение
TX-слайса = смена указателя, без Close/Open — не даёт щелчков), слайс в
FM RAW можно назначать источником передачи, персист in_dev_name.
Плюс удобства AUD-панели: открывается на выбранном устройстве и
прокручивается колесом мыши.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:22:23 +03:00
ew8bakandClaude Opus 4.8 8b5faaf986 chore(audio): убраны временные зонды PortAudio
other/padump.lpr, patest2.lpr, patest3.lpr писались для диагностики
зависания при выборе входа на слайсе (двойное открытие устройства, дамп
PA-списка). Баг закрыт как невоспроизводимый, зонды в сборку не входят и
ниоткуда не вызываются — при надобности достаются из истории (4aa180e).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:21:39 +03:00
ew8bakandClaude Opus 4.8 c986232891 ui(vfo): колесо мыши прокручивает список устройств в AUD
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>
2026-07-22 09:29:32 +03:00
ew8bakandClaude Opus 4.8 fb1c263460 ui(vfo): AUD-панель открывается на выбранном устройстве
При раскрытии fly-out AUD (и при переключении вкладок OUT/IN) список
теперь прокручивается к текущему устройству — выбранная строка ставится
примерно в середину видимого окна, а не приходится искать её стрелками.
Ручной скролл при этом не сбрасывается: push списков от контроллера
(SetAudioDevices/SetAudioInDevices) позицию не трогает.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:25:41 +03:00
ew8bakandClaude Opus 4.8 c69d537852 feature(tx): слайс в FM RAW можно назначать источником передачи
TX-бейдж на флаге слайса был заблокирован и для DMR, и для FM RAW, хотя тракт
FM RAW передавать умеет: SetTxSlice отбрасывает только DMR, а SetTXMode уводит
FM RAW в WFM с плоской девиацией. RX-only остаётся один DMR (нет AMBE-кодера).

- VfoOverlay.TxSelectionAllowed: запрет сузился до DMR.
- RadioController.SetSliceMode: если режим меняют у слайса, который сейчас
  источник TX, передающий тракт перестраивается под новый режим. Без этого
  выбор FM RAW на уже выбранном TX-слайсе оставлял в эфире прежнюю модуляцию
  до следующего клика по бейджу. На переходах в/из FM RAW заодно пересматривается
  источник модуляции (внешний PCM) со сбросом кольца входа — как в главном
  SetMode.

Заодно снята временная диагностика [MIC] (MicDbg и её вызовы) из mic-пула:
зависание при выборе на слайсе входа из настроек больше не воспроизводится.

Проверено на железе: TX со слайса в FM RAW работает. Подробное тестирование —
следующим шагом.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 23:01:59 +03:00