Commit Graph
157 Commits
Author SHA1 Message Date
ew8bakandClaude Opus 5 c2fa1da2b0 perf(dsp): молчащие слайсы считались от одного факта, что поднят TCI-сервер
RX_AUDIO по TCI снимается ДО громкости и мьюта, поэтому замьюченный слайс
обязан продолжать считаться, пока его кто-то качает по сети — иначе клиент
остаётся без звука ровно потому, что оператор убрал его у себя в комнате. Гейт
в ProcessSlicesFor это и делал, но спрашивал ГЛОБАЛЬНЫЙ флаг «тапы аудио
установлены», а его взводит SetTaps(True) на старте сервера. То есть галка
«Enable TCI server» одна, без единого клиента, заставляла гонять полный
fexchange0 по каждому молчащему слайсу — до шести штук, ~47 блоков в секунду на
каждый, плюс два входа в критические секции на блок в DSP-потоке. Клиент,
качающий один слайс из шести или вообще только IQ, оплачивал остальные так же.

Флаг стал ПОСЛАЙСОВЫМ (TSliceChan.TapWanted, SetSliceTapWanted). Кто его
выставляет — владелец потоков: TTCIAdapter.PushTapWants полностью пересчитывает
картину по FStreams и раскладывает по слотам. Считаются только потоки RX_AUDIO:
LINE_OUT и рекордер снимаются ПОСЛЕ мьюта, и на молчащем слайсе им достаётся
тишина независимо от того, посчитали его или нет.

Пересчёт зовётся отовсюду, где множество потоков или карта слайсов меняются:
StartStream (синхронно и ДО ответа клиенту — тогда не теряется ни один блок),
StopStream, DropClientStreams, DropDeadRxStreams, StopAllStreams и OnState по
MapChanged — слот мог сменить хозяина, и разрешение обязано переехать вместе с
ним. Список слушателей снимается под FStreamLock, и лок отпускается ДО похода в
движок: DSP-поток входит в тап, уже держа FSliceLock движка, и берёт в нём
FStreamLock — обратный порядок дал бы клин.

Глобальные FAudioTapsActive/SetAudioTapsActive/PushAudioTapsActive удалены.
FAudioTapCount остаётся: он по-прежнему гейтит сам вызов тапов (FireAudioTap) и
сборку звука DMR. Из AttachEngineCallbacks флаг не восстанавливается намеренно —
слайсы пересозданный движок не наследует вовсе.

Заодно: StopStream завершал перебор через Exit прямо из-под try, минуя хвост
процедуры, — заменено на Break.

Стенд (+3 проверки, негативный контроль в обе стороны): мьют слайса не глушит
RX_AUDIO по TCI; без потока молчащий слайс не считается вовсе (наблюдатель —
обычный аудио-тап, который теперь права требовать демодуляции не имеет);
попросили поток снова — слайс снова в работе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:07:25 +03:00
ew8bakandClaude Opus 5 84b90b23c0 feat(tci): KEYER — чужой ключ очередью элементов, а не сетевыми фронтами
Последняя невыполненная команда протокола. Ключ к ней в том, что arg3 —
длительность интервала, который ТОЛЬКО ЧТО кончился, а не начинающегося.
Документ задаёт это алгоритмом: первое нажатие keyer:0,true,0, отпускание
keyer:0,false,142 («посылка длилась 142 мс»), следующее нажатие
keyer:0,true,58 («пауза длилась 58 мс»). Отсюда перевод: тип элемента — это
состояние ключа ДО фронта, то есть обратное пришедшему; arg3 = 0 играть нечего.

Почему не дёргать ключ по приходу пакета: приход говорит, что интервал
кончился, а не сколько он длился, и манипуляция «по приходу» — это сетевой
джиттер прямо в эфир, тот самый «пьяный матрос», ради которого третий аргумент
в протоколе и появился. Новый TCWElemPlayer (CWMorse.pas) держит очередь
элементов и играет их подряд по абсолютным дедлайнам: сумма длительностей
равна времени у клиента, значит отставание постоянно (сеть + один элемент) и
не накапливается. Очередь опустела — ключ отпускается и следующая пачка
начинается с чистого дедлайна: элементы чередуются, искажения нет, зато
оборвавшийся клиент не оставляет в эфире несущую.

Контроллер: CWKeyerElement(Mark, Ms) ставит элемент в очередь, CWElemKey
раздаёт фронты — чужая манипуляция это прямой ключ с точными длительностями,
поэтому у Pluto она идёт во вход прямого ключа локального генератора (он сам
поднимает сессию, рисует огибающую и сайдтон), а у openHPSDR в бит CWX
прошивки (тем же путём идёт передача текста). Гейт CWTXActive, как у CWXSend;
обрыв общий с текстом — касание манипулятора, снятие MOX и уход из телеграфа
гасят чужую манипуляцию тем же CWXAbort. Передачу KEYER не поднимает: при
break-in PTT даёт прошивка (или сессия генератора), без него оператор держит
MOX сам.

Адаптер: номер передатчика разбирается как у TRX (bad receiver / receiver is
not running), захват §3.5 общий с TRX — передатчик один, и ключ держит тот же,
кто держит эфир. Паузы обрезаются TCI_KEYER_GAP_MAX_MS = 1 с (пауза целиком
прибавляется к отставанию от клиента, а дольше секунды — это «оператор
задумался», и честнее догнать реальное время), посылки — 5 с.

Стенд test/tci: 238 проверок (было 219). Новая часть C2 меряет ДЛИТЕЛЬНОСТИ по
фронтам ключа (посылка 150 / пауза 60 / посылка 150, допуск 30 мс), проверяет
отпускание на пустой очереди, старт следующей пачки без «догона» дедлайна,
обрыв и нулевую длительность; в части D — разбор аргументов команды и сквозная
проверка, что keyer:0,true,<мс> ключ не замыкает (это пауза), а
keyer:0,false,<мс> замыкает. Негативный контроль на инверсию перевода.
★Локальный генератор вооружается только при живом устройстве, поэтому сквозная
проверка подставляет FDevConnected/FRunning на время.

doc/TCI.md: §2.6 описывает команду целиком, §3.1 и §4 переписаны под то, что из
пары KEYER/TX_FOOTSWITCH остался только второй; попутно убран устаревший абзац
§2.3 про «потоки — этап 2».

На железе с настоящим ключом по сети ещё не гонялось.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:46:37 +03:00
ew8bakandClaude Opus 5 bb48f5d3fa feat(tci): приёмник = слот слайса; TRX/TUNE адресуют передатчик; стенд «как MSHV»
Модель приёмников переделана: приёмник TCI — это СЛОТ СЛАЙСА, а не панадаптер.
rx0 — главный тракт (каналы A/B = VFO A/B), rx N — слайс слота N−1, то есть
буквы B..G с флага, на каком бы пане он ни стоял. Панорама стала свойством
приёмника: от неё берутся DDS (у слайса только чтение) и поток IQ.

Почему: у Pluto панорама ровно одна (MaxPans = 1), и второй приёмник там
существует ТОЛЬКО как слайс главного пана — при нумерации по панам он был
недоступен вовсе, а TRX_COUNT навсегда равнялся единице. С другого конца —
клиенты: у MSHV в настройках всего «TCI Client rx1/rx2», то есть приёмники 0 и
1, третьего номера ввести некуда. Со слотами правило «первый созданный слайс =
приёмник 1» держится на любом железе: на openHPSDR слайс второго пана и на
Pluto слайс главного одинаково занимают слот B. Номер совпадает с буквой на
экране и с портом слайс-CAT. Цена: панорама без слайсов из TCI пропала, а
второй слайс пана перестал быть «каналом B» и стал своим приёмником — у канала
B в протоколе только частота, IF и громкость, у приёмника же всё.

TRX/TUNE раньше игнорировали arg1 (номер передатчика) целиком: клиент доп.
приёмника уводил в эфир слайс ОПЕРАТОРА — чужая частота, а с кросс-бандовым
мультислайс-TX и чужой диапазон, с чужими антенной и фильтрами; trx:9,true жал
PTT. Теперь номер разбирается и проверяется, приёмник N > 0 идёт через
RequestSliceTx (та же дверь, что у CAT-порта слайса: «в эфире только один» и
Auto TX), у контроллера появился параметр Tune для TUN тем же путём. Чужую
передачу не трогаем вовсе — ни источник модуляции, ни тон: SetMOX(True) поверх
идущей передачи не выходит рано, а заново выбирает микрофон. Хозяином эфира
клиент становится, только если передача началась именно от его команды, и
решает это Sync-метод в потоке контроллера (снимок «шла ли передача», взятый в
потоке клиента, врал: между разбором и исполнением влезает PTT оператора).

Разбор исходников MSHV (он фильтрует ВСЕ строки и бинарные блоки по номеру
приёмника) дал ещё три правки:
  * ответ на TRX/TUNE адресуется номером АВТОРА, состояние в нём — «в эфире
    именно твой слайс»; в рассылку идёт номер реально передающего;
  * tx_enable рассылается каждому живому приёмнику со своим номером и входит в
    картину нового приёмника — без этого у MSHV молча мёртвая PTT
    (set_ptt начинается с `if (!tci_tx_enable) return;`);
  * про несуществующий приёмник молчим целиком (LiveRx), в том числе на чтение:
    ответ «vfo:1,0,0» MSHV принимал бы за конец инициализации.

Попутно, вне TCI: SendDUCSpecificFromSettings трогала FNetwork без Assigned, а
зовут её по любому PTT/TUN (SetMOX → SyncCWKeyer → она) — до подключения
устройства это была Access violation, у TCI её глотал обработчик команды.

Стенды: новый test/tci/mshv_sim.py — точная копия логики клиента MSHV, отвечает
на вопрос «почему он не подключается» одной строкой (на живом приложении
воспроизвёл ошибку инициализации для rx2 до правки). tcitest — 158/158, в
сквозном прогоне добавлено создание слайса на главном пане: он становится
приёмником 1, отвечает на vfo:1,0, слушается командой, отдаёт аудио с
receiver = 1 и замолкает после удаления.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 22:10:02 +03:00
ew8bakandClaude Opus 5 dc6f996e70 fix(tci): дефекты живого прогона — length аудио, маршруты тапов, MOX, рекордер, EOF сокета
Восемь дефектов, найденных прогоном настоящего TCI-клиента (три приёмника:
NFM, DIGU, FMRAW) и его отчётом.

1. UI доп. панорам не перерисовывался: rfSliceState рассылался, но ветки в
   MainForm.OnControllerState не было (частоту несёт отдельный rfSliceFreq).
2. Stream.length у аудио — сэмплы НА КАНАЛ (§4.3), у IQ — вещественные
   отсчёты (§3.4: комплексных = length/channels). Было ×каналы везде, у
   стерео получалось вдвое больше. Развилка в TCIFillHeader + разбор
   TX-аудио в HandleBinary.
3+4. Дыры в маршрутах аудио движка: demod-тап звался только для DMR/FMRAW
   (у DIGU не было RX_AUDIO), а пост-громкостный — только для нецифровых
   (у FMRAW не было LINEOUT). Плюс мьют слайса больше не убивает RX_AUDIO:
   движку сообщают SetAudioTapsActive.
5. Клиент, поставивший TRX, уходил — MOX оставался. FTrxOwner + StopTxOf;
   TCIMicRequested снимается и по окончании любой передачи.
6. Гонка снятия IQ-тапа: SetIQTap(nil) возвращался раньше, чем DSP-поток
   выходил из вызова. FIQTapLock (порядок FSliceLock → FIQTapLock).
7. Рекордер был кольцом «последние N секунд», а §4.3 говорит про
   МАКСИМАЛЬНОЕ время записи с удалением по истечении. Переделан в линейный
   буфер с окном по часам от START.
8. TCIServer.HandleClient считал recv = 0 таймаутом: ноль — это EOF, errno
   при нём не трогается и несёт EAGAIN от прошлого истёкшего TCI_POLL_MS.
   Обычный TCP-разрыв без close-кадра не освобождал слот до остановки
   сервера, и после нескольких аварийных отключений новые клиенты упирались
   в TCI_MAX_CLIENTS. Теперь R = 0 рвёт связь безусловно, errno спрашивается
   только при R < 0.

Попутно: MainForm.RecreateDSPEngine (смена sample rate до START) терял
внутренние колбэки контроллера — введён AttachEngineCallbacks.

Стенд test/tci заведён в репозиторий (run.sh, 126/126 зелёных, включая
сквозной прогон через живой WDSP), доп. проверки на оба пути отключения
клиента. doc/TCI.md приведена в соответствие: правило про recv = 0 в §1.1,
единицы Stream.length, линейный буфер рекордера.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:10:29 +03:00
ew8bakandClaude Opus 5 de0830fd19 feat(tci): этап 2 — бинарные потоки IQ/аудио, TX-аудио и запись линейного выхода
Реализованы все четыре потока §3.4 и рекордер:

  • RX_AUDIO_STREAM — тап ДО громкости и мьюта (OnDemodAudioReady): скиммеру
    и цифре нужен звук приёмника, а не то, что осталось после ручки;
  • LINEOUT_STREAM — тап ПОСЛЕ (OnAudioReady), то есть что слышно;
  • IQ_STREAM — тап сырого IQ в движке, ОДИН вызов на накопленный блок;
  • TX_AUDIO_STREAM + TX_CHRONO — TRX:0,true,tci берёт модуляцию из потока
    клиента (флаг TCIMicRequested впереди web в SetMOX), маркеры времени идут
    из тика по часам, аудио клиента разворачивается в 48 кГц моно в тот же
    ринг, что и web-микрофон;
  • LINE_OUT_RECORDER_* — кольцо int16 на приёмник, WAV пишет отдельный поток.

Тапы аудио в контроллере многоадресные (AddAudioTap): слушают, ничего не
забирая, в отличие от OnAudioConsume, которым владеет web. Блоки нарезает и
раскладывает по кольцам клиентов сам DSP-поток, в сокет пишет поток клиента —
та же дисциплина, что у команд. Порядок локов везде FSliceLock → FStreamLock.
У очереди команд и кольца блоков разная политика переполнения: команду терять
нельзя, блок потока — можно (теряется самый старый).

Пересчёт частоты многоступенчатый (TCIStreams). Одноступенчатый FIR на верхнем
пресете Pluto (5760 кГц, коэффициент 120) упирался в потолок отводов и давал
завал 1.3 дБ в полосе при подавлении зеркала 16 дБ — то есть поток IQ с
мусором. Теперь коэффициент раскладывается на множители, спецификацию фильтра
каждой ступени задаёт ИТОГОВАЯ полоса, а свёртка идёт со сложением
симметричных пар: −83 дБ на любом коэффициенте, ≈10% ядра на 5.76 МГц.

Согласование частот с железом: из пресетов Pluto 576 и 960 кГц на 384 не
делятся, поэтому отдаём наибольшую ЗАКОННУЮ частоту, делящую источник нацело
(576/960 → 192 кГц). Ответ на IQ_SAMPLERATE называет достижимое, а не просьбу
клиента, и переобъявляется без запроса при смене rate и устройства.

Приёмный буфер соединения 4 → 32 КБ: блок TX-аудио это 64 байта заголовка плюс
data[16384], а кадр крупнее буфера не собирается никогда.

Настройки TCI переехали из Advanced на вкладку CAT, справа от TCP CAT Server:
это такой же канал внешнего управления трансивером.

Стенд (scratchpad, tcitest.pas): 112 проверок, все зелёные — включая сквозной
прогон через живой WDSP (синтетический IQ → блоки RX-аудио и IQ у настоящего
WS-клиента, и обратно TX-аудио клиента → блоки TX-IQ).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:36:50 +03:00
ew8bakandClaude Opus 5 4165cbe9a5 fix(tci): ревизия — потоки, валидация, арбитраж и синхронизация клиентов
Разбор семи проходов ревью ветки. Ниже — по сути, а не по списку.

Потоки. Сетевые потоки больше не читают модель контроллера напрямую. Слайсы
снимаются в потоке контроллера (RefreshSlices → FSliceSnap, на событиях
rfSliceFreq/rfSliceState/rfDevice/…), железо — тоже (RefreshDev → TTCIDevSnap:
имя платы, границы, число панов, HasTX). Копия TCtrlSlice из чужого потока
портила счётчик ссылок managed-строк, а BackendCaps и BoardDisplayName смотрят
в FNetwork, который UI освобождает на смене устройства. По той же причине
ActiveTXFreqHz переведён на GetSliceView. Sync-методы читают живую таблицу: они
уже в потоке контроллера.

Жизненный цикл. Stop ждёт выхода клиентских потоков БЕЗ таймаута, прокачивая
очередь Synchronize: выйти по таймауту нельзя — следом освобождаются и клиенты,
и сам сервер. OnDisconnect зовётся и при остановке (иначе захваты параметров
ушедших клиентов доживали до следующего запуска). Отправка переехала на поток
самого клиента (recv с TCI_POLL_MS): общий поток задерживал всех на таймаут
записи в один медленный сокет. WebUtils.SockSend шлёт с MSG_NOSIGNAL — SIGPIPE
убивал headless-процесс.

Транспорт. Слот протокола выдаётся только после Upgrade, а сокет до него живёт
по таймауту handshake: восемь молчащих соединений закрывали дверь настоящим
клиентам. Handshake с заголовком Origin получает 403 — авторизации в TCI нет, и
без этого открытая вкладка браузера дотягивалась до TRX и VFO. Заголовки
разбираются построчно, текстовые кадры проверяются на UTF-8, close длиной один
байт отвергается, на close отвечаем close.

Валидация. Все установки ходят через TCITryArg* — «vfo^0~0~abc» больше не
превращается в честный ноль. Частота проверяется дважды: в потоке клиента по
снимку и в SyncSetVfo/SyncSetCenter по живым границам (устройство успевают
сменить между разбором и исполнением). Границы теперь из ОДНОГО источника
(FreqLimits поверх VisibleFreqBounds) — тот же, что уходит в VFO_LIMITS; сами
VFO_LIMITS переобъявляются при смене железа, и их кэш ведётся независимо от
того, подключён ли кто-то. Слайс двигается только TuneSliceInBand, как у CAT:
прямой SetSliceTarget уводил TX-слайс в DUC на чужой диапазон без антенн и
фильтров. Параметры потоков сверяются со списками спецификации, а IQ_START и
прочие запуски честно отвечают ошибкой вместо молчания.

Синхронизация клиентов (§3.5). Появился захват параметра на 200 мс: два логгера
больше не перетягивают частоту. Пачка инициализации уходит под FClientLock —
изменение между строкой снимка и READY терялось навсегда. Глобальные величины
(tune_drive, cw_macros_*, split_enable, mon_volume) рассылаются всем, а правки
оператора приходят событиями: rfTXProfile, rfActiveVfo, rfMonVolume и новый
rfCWSettings. Создание и удаление слайса рассылается по rfDevice (сравнение
расстановки), у живого пана без слайсов канал A показывает центр — иначе клиент
навсегда оставался с частотой удалённого слайса.

Прочее. SliceFreqChanged переехал внутрь SetSliceTarget — один путь для мыши,
CAT и TCI (перетаскивание флага мимо клиентов проходило молча). VOLUME и
MON_VOLUME развели: SetVolume правит АКТИВНУЮ громкость, поэтому команда на
DUP-передаче уезжала в монитор — добавлен адресный SetRxVolume. Настройки
сохраняются только после успешного применения, при отказе поднимается прежний
слушатель. Время спота — UTC. Подписки на измерители читаются и пишутся под
локом клиента.

Проверено стендом (сырой WS-клиент + живой TRadioController без железа):
73 проверки, включая изоляцию медленного клиента, остановку под Synchronize,
арбитраж до и после 200 мс, отбраковку по живым границам и переобъявление
VFO_LIMITS. На реальном железе и с реальным клиентом по-прежнему не гонялось.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:53:32 +03:00
ew8bakandClaude Opus 5 84c9e60b93 fix(tci): жизненный цикл, синхронизация слайсов и WebSocket по RFC
Разбор ревью ветки. Критичное — четыре отказа жизненного цикла и один
пробел синхронизации.

Use-after-free стора спотов: FTCIAdapter освобождается ДО FDXStore.
Команда SPOT/SPOT_DELETE, пришедшая между их гибелью, обращалась к
освобождённой памяти.

Bind-адрес: TCIParseIPv4 стал строгим (out + Boolean, ровно четыре
октета 0..255). Кривой адрес — отказ поднимать сокет, а не молчаливый
INADDR_ANY: авторизации в TCI нет. В UI порт и адрес применяются по
уходу фокуса и по Close, а не на каждую букву — набор «127.0.0.1» по
дороге проходил через «127.0.0.» и открывал порт наружу.

Остановка при висящем Synchronize: флаг Stopping (адаптер не начинает
новых Invoke), прокачка CheckSynchronize в цикле ожидания Stop и запрет
освобождать клиента, чей поток не вышел. Владение переделано: клиента
освобождает только тик-поток (ReapClients), клиентский лишь помечает
себя закрытым.

Отправка больше не блокирует вызывающего: Send/Broadcast кладут строку
в очередь клиента, в сокет пишет тик-поток вне общего лока, склеивая
очередь в общие кадры. Медленный клиент морозил UI на таймаут отправки
за каждое движение ручки VFO; теперь он просто вылетает.

Слайсы: в контроллере появилось rfSliceState (нагрузка — FSliceFreqId),
его шлют сами сеттеры слайса; SyncSetVfo зовёт SliceFreqChanged, как
CAT. Адаптер разворачивает Id в пару (приёмник, канал) и рассылает
состояние именно этого канала, а не канала 0 каждого пана.

WebSocket по RFC 6455: маска обязательна, FIN/continuation собираются,
RSV и незнакомые opcode рвут соединение, control-кадры ≤125 и только
целиком, 64-битная длина не сворачивается в отрицательный Integer,
пустой Sec-WebSocket-Key получает 400. Хвост пакета handshake больше не
выбрасывается — первая команда не теряется. Клиент после исключения в
разборе не остаётся висеть в массиве.

Ещё: DSP и squelch доп. приёмников читаются и пишутся из TCtrlSlice
(парные сеттеры сохраняли соседние поля значениями главного тракта);
параметры потоков — в TTCIClient, они клиентские по спецификации; эхо
под своим локом; ApplySettings возвращает результат, отказ старта виден
оператору; инициализация объявляет только существующие каналы;
SET_IN_FOCUS реализован через OnFocusRequest.

Осознанно не сделано и записано в doc/TCI.md §3.1: AGC_GAIN для
приёмников >0 (AGC-T один на тракт), цвет спота, KEYER, TX_FOOTSWITCH,
арбитраж нескольких клиентов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:49:31 +03:00
ew8bakandClaude Opus 5 82f0e4f771 feat(tci): TCI 2.0 — EWSDR как сервер (команды и уведомления)
Протокол Expert Electronics поверх WebSocket, порт 40001. EWSDR слушает,
клиенты — логгеры, скиммеры, цифровые программы.

- TCIProtocol.pas — чистый слой протокола: разбор/сборка `имя:арг;`,
  экранирование `^ ~ *`, словарь видов связи, пересчёт громкости в дБ.
- TCIServer.pas — WS-сервер поверх WebUtils/WsClient: accept-поток,
  поток на клиента, HTTP-Upgrade, фреймы, рассылка, тик 20 мс.
- TCIAdapter.pas — мост к TRadioController по схеме CAT: геттеры читают
  поля напрямую, сеттеры через Invoke, уведомления через AddStateListener.
  Маппинг: приёмник TCI = панадаптер, канал A/B = VFO A/B (пан 0) либо
  первый/второй слайс (паны 1..).

Попутно: TRadioController.RemoveStateListener (адаптер умирает раньше
контроллера) и TDXSpotStore.RemoveCall (spot_delete).

Настройки — секция "tci" в settings.json (умолчание: выключено,
127.0.0.1, так как авторизации в протоколе нет) и вкладка
Advanced → TCI Server.

Бинарные потоки (IQ/аудио) — этап 2. Статус, таблица команд и список
осознанных эхо-заглушек — в doc/TCI.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:22:49 +03:00
ew8bakandClaude Opus 5 f7bed07831 refactor(controller): единая дверь SetTXSettings для правки TX-настроек
Применение TX-настроек (WDSP, DUC Specific при смене mic-битов/ATT, живое
обновление TUN, запись в активный профиль, персист) жило в обработчике
MainForm — то есть было недоступно ничему, кроме вкладки Transmit. CAT и web
по архитектуре ходят только в контроллер и дотянуться туда не могли.

Логика переехала в TRadioController.SetTXSettings по образцу уже
существовавшего SetCWSettings; MainForm делегирует ей и оставляет себе только
рендер. Notify=False у формы — иначе rfTXProfile перезагрузил бы вкладку
Transmit прямо под руками у того, кто её правит.

Заодно SetTXMonVolume: громкость self-monitor'а адресно, вне TX-контекста
(слайдер добирается до неё только на передаче, CAT-клиенту такой контекст
не нужен).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 13:12:10 +03:00
ew8bakandClaude Opus 5 8569236214 feat(beacon): автопоиск маяка в окне лупы (кнопка AUTO)
Лупа сделала прицеливание возможным, но целиться всё равно надо руками. AUTO
находит маяк сам, в текущем окне лупы: что видно, то и обыскивается — заодно
зумом можно сузить область поиска.

Критерий — не «самый громкий», иначе поймается первая же SSB-станция. Копим
min-hold по кадрам лупы (~1.6 с): у маяка несущая непрерывна и минимум остаётся
высоким, а речь, телеграф и цифра в паузах проваливаются к шуму и срезаются
минимумом. Это тот самый «непрерывный след», который видно на водопаде, только
числом.

По накопленному: шумовой пол = медиана, кандидат = максимум скользящего среднего
шириной с полосу приёма (±450 Гц) — среднее по полосе, а не отдельный бин, потому
что маяк это плато ~900 Гц, и по такой мере он выигрывает у узкой пораженки с той
же высотой пика. Порог 6 дБ над полом; центр уточняется центроидом превышения,
чтобы наведение село на середину плато.

- Тумблер рядом с FOLLOW, состояние в настройках (beacon_zoom_auto).
- На локе автопоиск молчит: в контур не лезем.
- После наведения выдержка ~3 с — дать декодеру попробовать захватиться.
- Нашли там же, где уже стоит наведение (ближе 150 Гц) — не трогаем.
  BeaconSeedAtHz дёргает Reseed, и без этой проверки при слабом сигнале
  автопоиск сбрасывал бы захват по кругу, мешая декодеру сойтись.
- В накопление идут только свежие кадры: таймер формы (25 Гц) быстрее
  анализатора (15 к/с), и повторный учёт кадра сокращал бы окно наблюдения,
  ради которого min-hold и заведён.

В строке захвата появляется пометка AUTO — и только пока автопоиск реально
работает: на локе она гаснет, чтобы не обещать действие, которого нет.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На эфире не проверено.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 13:01:26 +03:00
ew8bakandClaude Opus 5 51599b2457 fix(beacon): контур принимал за маяк любую узкую несущую
Наводишься на соседнюю станцию — и контур честно берёт её за маяк и тащит
LOError, подтягивая чужой сигнал к 10489.750.

Решение принималось по CarrierLock и SNR, а CarrierLock = FCarPresent and
FFreqLocked. FCarPresent — prominence сильнейшей линии в squaring-FFT: возведение
в квадрат схлопывает модуляцию BPSK ±180° в одну линию на 2·fc, но такую же линию
даёт любой сигнал с узкой составляющей — телеграф, пораженка, остаток несущей у
цифры. FFreqLocked — Costas держит фазу, а на немодулированную несущую он садится
идеально: ровный тон это BPSK без переходов. SNR считается как (E|I|)²/E[Q²], и
захваченная Costas'ом несущая лежит целиком на оси I — Q пустой, SNR выходит
отличный. То есть чужой сигнал не просто проходил порог, он выглядел лучше маяка.

Различаем по констелляции: у настоящего BPSK точки ходят между двумя сгустками
±I (данные после свёрточного кодера равновероятны), у несущей все в одном.
BeaconIQBalance считает долю меньшинства по знаку I, EMA сглаживает, порог 0.15
на взятие лока и 0.08 на сброс — гистерезис, как у SNR. Кольцо в 256 точек при
400 Bd набирается за ~0.6 с, EMA добавляет столько же: чужой сигнал отваливается
примерно за секунду, а не за десяток, как ждать кадр FEC.

FBeaconBal обнуляется вместе с FBeaconLocked при каждом наведении и при включении
лока — после клика BPSK-ность доказывается заново, унаследовать её от прошлой
цели нельзя.

Скорость символов признаком служить не может: FWsym зажат в ±0.02, поэтому
S.SymRate конструктивно всегда 392..408 Bd, что бы декодер ни слушал.
Непрерывный цифровой сигнал с настоящей модуляцией баланс пройдёт — от него
защитит только декодированный кадр AO-40.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На эфире не проверено.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:59:46 +03:00
ew8bakandClaude Opus 5 e82f573313 feat(beacon): окно лупы переживает перезапуск
Ширина, положение и состояние FOLLOW уезжали в дефолт при каждом открытии
окна: настроил 5 кГц вокруг маяка, закрыл — в следующий раз снова 40 кГц.

Положение хранится СДВИГОМ от опорной частоты маяка, а не абсолютом. Абсолют
протух бы от любой правки опорной частоты, а главное — калибровка LOError
копится от сеанса к сеансу и двигает шкалу, так что сохранённая абсолютная
частота через неделю указывала бы уже не туда.

- Settings: BeaconZoomSpanHz/OffsetHz/Follow в TGlobalSettings (ключи
  beacon_zoom_span/offset/follow), дефолты и чтение/запись по общему пути.
- RadioController: FBcnZoom* с клампом при загрузке (span 600..200000 Гц,
  сдвиг ±500 кГц) — руками испорченный JSON окно не сломает.
- BeaconScopeForm: RestoreZoomState в DoShow, StoreZoomState каждый тик из
  PollZoom. Раз состояние пишется в одном месте, оно не разъедется, каким бы
  путём ни менялось — колесом, drag'ом, кнопками или автоподтяжкой FOLLOW.
  Наведение декодера за краем восстановленного окна подтянет сам FOLLOW на
  первом тике, отдельной логики не потребовалось.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:03:13 +03:00
ew8bakandClaude Opus 5 705da75c26 feat(beacon): лупа наведения на маяк QO-100 в окне Beacon
На спектрограмме 576к центральный маяк — пара пикселей, ткнуть в него для
наведения декодера почти нельзя. В окне Beacon появилась карточка BEACON
TUNING: узкий кусок спектра вокруг маяка в высоком разрешении + мини-водопад,
клик по нему = BeaconSeedAtHz, ровно как клик по главному спектру. Наведение
с главной спектрограммы не тронуто.

Увеличение делает сам WDSP, а не растяжка бинов главного дисплея: второй
analyzer BCN_DISP_ID=4 на том же IQ главного тракта, со своей FFT и своим
span-clip. Клипы fscLin/fscHin считаются прямо из желаемых границ окна в
герцах, а не из зум-слайдера — анализатор умеет ассиметричный клип, поэтому
окно ставится в любое место полосы захвата, а не только вокруг центра DDC.

- WDSPEngine: SetBeaconZoom + GetBeaconZoom{Spectrum,Waterfall}. Analyzer живёт
  только пока окно маяка открыто; весь доступ к disp'у под FBcnLock (SetAnalyzer
  из UI, Spectrum0 из DSP-потока, GetPixels из display-потока). Кадр (1024
  точки) фиксирован по размеру — ресайз окна не переармирует analyzer и не
  морозит спектр на наполнение FFT-окна.
- Span-clip НЕ удешевляет FFT — она по всей полосе захвата. Отсюда потолок
  262144 и своя пониженная частота кадров BCN_ZOOM_FPS=15 через ovrlp: на 60 fps
  это было бы ядро под нагрузкой ради картинки, в которой ничего не меняется.
  Переарм по смене sample rate, детектор/усреднение — без переарма.
- RadioController: SetBeaconZoomWindow / GetBeaconZoom* / GetCaptureWindow /
  BeaconZoomBinHz. Наружу — абсолютные display-Гц, те же, в которых живут
  BeaconSeedAtHz и маркеры главного спектра.
- BeaconScopeForm: клик = наведение, колесо = зум вокруг курсора, drag = пан,
  двойной клик = центрировать, кнопки -/+/RESET/FOLLOW (FOLLOW подтягивает окно
  только когда маркер ушёл за край, а не возит картинку постоянно). Маркеры —
  теми же цветами и с той же семантикой, что DrawBeaconMarkersRaw/GL: опорная
  пунктиром, центроид, три линии полосы приёма ±450 Гц, без полупрозрачной
  заливки. Водопад — TWaterfallView с палитрой/гаммой из контроллера.
- Рендер по конвенции проекта: кадр лупы собирается в офскрин только по
  FZoomDirty (новый кадр анализатора, сдвиг маркеров/окна, ресайз, тема), Paint
  = блит + курсор. Констелляция с метриками переведена туда же: статичная
  обвязка (карточки, оси, рамки и подписи плиток) кэшируется в FScopeChrome и
  пересобирается лишь на ресайз/смену темы — раньше десяток RoundRect и TextOut
  рисовались заново 25 раз в секунду прямо в обработчике Paint.

Лупа рисуется на CPU: GL-путь в проекте есть только у главного панафолла и
только под ключом, все прочие панели — обычные TPaintBox.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На железе не проверено.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:42:25 +03:00
ew8bakandClaude Opus 5 00860e5154 fix(slice-cat): перестройка слайса внутри полосы захвата обновляет флаг
TuneSliceInBand в быстрой ветке (цель уже внутри захваченной полосы) звал
только SetSliceTarget и не слал ни одного Changed. Позицию флага UI держит
сам (TickFlags -> LayoutFlags читает живой TargetHz), а частота и кромки
фильтра живут в собственных полях оверлея и заливаются только из
PushSliceFlagState. Итог: флаг уезжал на новую частоту, показывая старые
цифры и старую полосу фильтра. Дальний QSY выглядел исправным лишь потому,
что там двигалось окно DDC и приходил rfPanFreq с PushAllSliceFlagStates.

Новое поле rfSliceFreq (+ FSliceFreqId как полезная нагрузка) и хелпер
SliceFreqChanged: шлётся из быстрой ветки и из ветки пана 0 (rfCenterFreq
флаги не перезаливает). MainForm обновляет только тронутый слайс.

Тракт передачи не менялся: ActiveTXFreqHz и так берёт TargetHz слайса-
источника, а SetSliceTarget сразу пушит сетевое состояние (DUC).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:46:05 +03:00
ew8bakandClaude Opus 5 b02b68aa6a feat(cw): терминал телеграфа — набор с клавиатуры и декодер приёма
Окно (ПКМ по CWL/CWU, F9, кнопка в настройках CW): одна лента, где принятое
декодером и СВОЯ передача идут вперемешку — своё акцентным цветом. Разделять их
нельзя: в QSK они перемежаются посреди фразы. Под лентой строка набора.

Печать уходит в эфир ПОСИМВОЛЬНО, а не по Enter: строка показывает ровно то,
что ещё не передано (очередь генератора), знаки уходят из неё по мере отправки,
Backspace стирает с хвоста очереди — то, что уже звучит, вернуть нельзя.
Ctrl работает манипулятором (левый точка, правый тире; у кейера прошивки
лепестков нет — там это прямой ключ через бит CWX), по умолчанию выключено,
чтобы Ctrl+C не уводил в эфир. Отпускание ключа ловится и на потере фокуса:
иначе уход из окна с зажатым Ctrl оставил бы несущую в эфире навсегда.

CWDecoder.pas: аудио → текст. Отвод берётся до громкости и мьюта — там нет
программного сайдтона, зато уже отработал узкий CW-фильтр, лучшего предетектора
не найти. Гёрцель гребёнкой из пяти бинов вокруг pitch (заодно показывает
расстройку) → огибающая → адаптивный порог с гистерезисом → длительности →
адаптивная точка → обратная таблица Морзе. Длительности живут в шагах анализа,
а не в показаниях часов: разбор идёт пачками с таймера, и привязка ко времени
вызова ломала бы тайминг на любой загрузке.

Вылезло на тестах и учтено: пик обязан клампиться не ниже пола шума (иначе на
старте порог уходит НИЖЕ шума и первым «знаком» читается собственный шум);
порог дребезга берётся от текущей точки, фиксированный либо пропускает щелчки
на медленной передаче, либо ест посылки на быстрой; длина посылки меряется за
вычетом подтверждения дребезга, иначе скорость занижалась на 15%; расстройка
запоминается только на полной амплитуде посылки, иначе индикатор пляшет.
Граница честная: при вдвое неверной подсказке скорости теряется первое слово —
пока не услышана настоящая точка, длина элемента неизвестна.

Лента расшифровки продублирована строкой под спектром (пан 0), эхо передачи
и очередь набора появились у обоих отправителей — и у локального генератора,
и у кейера прошивки.

★TFlatEdit получил публичный CaretPos: он вставляет знак сам в UTF8KeyPress и
гасит клавишу, поэтому OnKeyPress контрола не вызывается вовсе — из-за этого
набранное «исчезало», а в эфир не уходило. Ввод перенесён на уровень формы.

Проверено оффлайн: 12/20/40 WPM, расстройка, шум, слабый сигнал, цифры и знаки;
плюс сквозной прогон против шести станций CW-стенда в hpsdrsim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 21:28:48 +03:00
ew8bakandClaude Opus 5 25fbf10212 feat(cw): программный кейер — телеграф на Pluto/LibreSDR
У openHPSDR точки и тире формирует кейер в FPGA, и это остаётся путём по
умолчанию: тайминги в железе, джиттер PC не влияет вовсе. У AD936x такого
кейера нет вовсе, поэтому телеграфа на Pluto не было в принципе — голосовой
TXA в CW не запускается, а несущую давать некому.

CWKeyer.pas: TCWLocalKeyer рисует манипулированную несущую прямо в поток IQ,
мимо WDSP (эталон pihpsdr cw_shape_buffer: в WDSP режим TXA_CWL — ветка SSB,
да и PostGen не умеет огибающей). Длительности живут В СЭМПЛАХ, а не в
миллисекундах: планировщик ОС влияет только на темп выдачи блоков, а его
сглаживает FIFO бэкенда. Умеет текст, иамбик A/B и прямой ключ; TCWKeyPort
читает манипулятор с модемных линий COM/USB-serial (DTR/RTS питают ключ,
CTS/DSR/DCD/RI — лепестки). Оффлайн-прогон: PARIS занимает ровно 43 точки,
все посылки и паузы 1/3/7 dit с точностью до сэмпла.

Сессия = одна передача, а не одна посылка: PTT, реле T/R, антенна, OC и
аттенюатор Pluto поднимаются один раз и держатся до конца hang — внутри
манипуляция чисто цифровая. Предзаполнение FIFO задано железом: TX-поток
Pluto наполняет буфер целиком (16384 пары ≈ 28 мс на 576k) и добивает
нулями, чего не хватило. Сайдтон эту латентность не наследует — кольцо
огибающей листается, если отставание больше 20 мс.

Сдвиг несущей: у zero-IF ноль бейсбенда совпадает с частотой гетеродина, и
там же его утечка — манипуляция на нуле дала бы backwave ровно на рабочей
частоте. Поэтому несущая идёт на сдвиге, а гетеродин уезжает навстречу через
новую единую дверь TXTuneFreqHz (все пуши TX-частоты переведены на неё).
Знак (CW_TX_BB_SIGN) замерен на эфире, а не выведен.

Развилка источника в UI — один список Keyer: firmware / software (PC) / off;
хранится прежней парой флагов, и на железе без кейера выбор «прошивка»
вырождается в программный, поэтому Pluto работает из коробки. Новое поле
caps HasCWKeyer (раньше это неявно жило в HasHWMic).

Попутно: у AD9361 calib_mode стоял manual_tx_quad, то есть драйвер не
переигрывал калибровку TX quad (она нулит утечку гетеродина) на сменах
TX LO — выставляем auto при подключении.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:38:54 +03:00
ew8bakandClaude Opus 5 827902f3e2 fix(xvtr): правки в трансвертере больше не травят память КВ-диапазона
При входе в слот трансвертера FCurrentBand не меняется — он продолжает
указывать на тот КВ-диапазон, с которого зашли. Поэтому всё, что помнится
по диапазону, из трансвертера писалось в чужую ячейку: модуляция, слот
фильтра, режим и порог АРУ, CTUN. Поставил в трансвертере FM — 40 метров
запомнили FM.

Развилка «мы сейчас в трансвертере?» в проекте уже была, но стояла только
у двух писателей из семи — у шагового аттенюатора и у зума. Их правили
когда-то по симптому, отчего баг и возвращался: затыкали поле, а не класс.

Заведена XvtrSlotActive, через неё идут все оставшиеся писатели. Слот
трансвертера соответствующие поля уже имел (LastMode, LastFilterIdx,
LastAGCMode, LastAGCTop, LastCTun) — они просто не заполнялись живыми
правками, только снимком на выходе из слота.

SpanHz в кэше оставлен как есть: поле помечено легаси, при восстановлении
диапазона его никто не читает.

Уже испорченные записи конфига правка не лечит: чтобы починить диапазон,
надо встать на него, выставить нужную модуляцию и уйти — SaveCurrentBand
перезапишет ячейку.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:33:22 +03:00
ew8bakandClaude Opus 5 45d10698ee fix(cw): запрет передачи действует и на кейер прошивки (RX-only XVTR, DoNotTx)
Аудит трансвертерного режима против дефектов этой сессии нашёл ту же болезнь,
что и всё остальное: защита жила только на пути через софтовый MOX.

Бэнд с DoNotTx (Alex) и RX-only слот трансвертера проверялись тремя копиями
кода — в SetMOX, SetTune и SetTwoTone. С кейером в прошивке ни одна из них не
исполняется: на приёмном трансвертере замыкание ключа выходило в эфир, а там
на выходе обычно вход конвертера, а не антенна.

Три копии сведены в TXProhibited; CWTXActive её учитывает, поэтому байт 5
обнуляется и кейер разоружается там, где передавать нельзя. Передача текста
гейтится тем же условием.

Вооружение сделано самовосстанавливающимся: SyncCWKeyer идемпотентен (пара
сравнений плюс ранний выход внутри SetCWKeyerArmed) и вызывается ещё и из
разбора HP-статуса. Разоружить кейер может заход в RX-only слот, переход на
запрещённый бэнд или правка Alex; перечислять такие точки поимённо — гарантия
однажды пропустить очередную, на чём уже дважды обожглись за сегодня.

Остальное в трансвертере проверено и чисто: сдвиг pitch переживает
XvtrTranslateTX (включая split-LO QO-100), множитель мощности слота и
VHF-калибровка уже входят в CalcDriveByte и потому уезжают в байт 345,
OC-выходы идут за ключом через RadioKeyed, ActivateXvtr толкает верную частоту
DUC. Инвертирующие трансвертеры не поддержаны нигде в проекте — это давнее
общее ограничение, не регресс телеграфа.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:09:05 +03:00
ew8bakandClaude Opus 5 c6d1e409ff fix(cw): реле T/R и антенна за ключом прошивки + залипание HW-PTT
Отвязав FTransmitting от телеграфа, я оставил в приёмной конфигурации всё,
что бэкенд считает от состояния передачи: CalcAlex0 бит 27 (реле T/R),
выбор антенны (TxAnt вместо RxAnt), bypass/Ext1 и CalcOCBits, которым
ключуется внешний усилитель. Прошивка гнала несущую, пока плата фильтров
стояла на приём, и приёмник видел собственный передатчик — на водопаде это
широкополосные полосы в моменты манипуляции.

PushNetworkState отдаёт в UpdateState RadioKeyed, и он же вызывается на
фронтах ключа. PTT в кадре этим не поднимается (он живёт в SendPTT), так
что манипуляцией по-прежнему владеет FPGA. Выдержка RadioKeyed поднята до
hang + 250 мс: по ней теперь щёлкает реле, и щёлкать оно обязано раз на
передачу, а не на каждую посылку. FTuning из RadioKeyed убран — при TUN
и так взведён FTransmitting, а в разборе SetTune флаги гаснут не разом.

Второе: телеграфная ветка разбора статуса заканчивается Exit, и падающий
фронт HW-PTT обработать было некому. Если в эфир увёл именно HW-PTT, пока
источником передачи был не телеграф, а телеграф включился уже после, то
передача залипала навсегда — FTransmitting взведён, Alex в TX, PTT не
снимается. Подтверждено на железе: всплески появлялись после переключения
передачи на слайс и оставались после возврата на главный.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 13:54:58 +03:00
ew8bakandClaude Opus 5 0a1a1ee3af fix(cw): первый прогон на железе — частота, мощность, индикация, полоса
Семь дефектов, вскрытых проверкой в эфире. Общий корень у большинства: с
кейером в прошивке FTransmitting не поднимается вовсе — передачей владеет
FPGA, — а половина проекта считала этот флаг признаком эфира.

Обрыв посылки после первого элемента. При break-in железо само поднимает
T/R на каждую посылку, HPS_PTT это отражает, и разбор статуса гонял по
фронту SetMOX(True/False); снятие MOX гасило CWX. Теперь в телеграфе фронт
HPS_PTT не управляет передачей. Без этого и работа манипулятором
пересобирала бы передающий тракт на каждой точке.

Обрыв по «key hit» переведён на фронт лепестка: на плате без ключа входы
могут стоять в единице, и проверка уровня убивала бы передачу сразу.

Посылка уходила по центру дисплея, а не на VFO. StartRunning ставил DUC на
центр DDC; до первой перестройки VFO или до первого MOX там и оставалось.
Без CTUN центр совпадает с VFO, поэтому баг и дожил до телеграфа, где MOX
не бывает. SetRunAndFreq получает TX-частоту, SetSliceTarget толкает кадр,
когда перестраивают слайс-источник. Правило: DUC обязан стоять правильно
всегда, а не только на передаче.

Мощность ниже, чем на TUN: уровень (байт 345) клался в кадр только при
FIsTransmitting. Введён SetCWKeyerArmed — пока кейер вооружён, уровень
лежит в каждом HP-кадре. Там же пересчитывается FDriveLevel, иначе до
первого касания ручки в железо уходил ноль.

Индикация не показывала передачу: события rfTransmitting в телеграфе не
бывает, поэтому блок рендера не выполнялся ни разу. ApplyHPStatus шлёт его
сам на фронтах RadioKeyed («железо в эфире» = передача или замыкание ключа
прошивкой, выдержка 8 точек и не меньше 500 мс). На RadioKeyed переведены
S-метр, красная полоса главного VFO и TX-бейджи флагов вместе с красной
полосой слайса на панадаптерах.

Полоса на спектре рисовалась как SSB: TXSignedEdges обязана отдавать для
CW голосовую боковую (через bp0 идёт тон pitch при настройке), поэтому
экранные кромки развязаны в TXFilterEdgesHz — занимаемая полоса
манипуляции симметрично несущей. Тот же класс, что был у ЧМ.

F1..F8 молчали, пока не откроешь окно памяти: обработчик упирался в
проверку лениво создаваемой формы. Текст берётся из настроек.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 13:14:38 +03:00
ew8bakandClaude Opus 5 53329e82e8 feat(cw): телеграф — FPGA-кейер, pitch, сайдтон, передача текста
CW не работал ни на приём, ни на передачу: фильтр стоял симметрично вокруг
нуля (тона не было), понятия pitch не существовало, MOX в CWL отправлял в
эфир голос 2.9 кГц (в WDSP TXA_CWL — ветка SSB), байт CW-опций DUC всегда
был нулевым, поэтому кейер, сайдтон и break-in в прошивке молчали.

Pitch сделан ОДНИМ сдвигом в движке. Кромки фильтра везде остаются
относительно VFO (у CW — симметрично нулю), а гетеродин и полосу двигают
три метода WDSPEngine: CWLOOffset / PushShift / PushPassband. Прямых
вызовов SetRXAShiftFreq и RXASetPassband в проекте больше нет. Так таблица
фильтров не перестраивается при смене pitch (в отличие от Thetis), полоска
на спектре и маркер VFO верны без правок в UI, а слайсы получают свой
сдвиг по своему режиму.

Правила эфира:
- голосовой TXA в CW не запускается вообще (гард по режиму), MOX = только
  PTT; несущую даёт кейер, бит CWX или тон TUN;
- байт 5 собирается из настроек и перепосылается на каждой смене режима и
  TX-слайса — вне CW он обязан быть нулевым, иначе прошивка поднимет PTT на
  замыкание ключа посреди SSB;
- TUN в CW: тон = pitch и встречный сдвиг DUC, несущая встаёт ровно на VFO;
- приёмник на передаче в CW не глушится, иначе умолкает программный
  сайдтон и эфир между посылками.

Программная передача текста (CWMorse): поток с абсолютными дедлайнами
дёргает бит CWX в High Priority, элементы рисует прошивка. Касание
манипулятора или снятие MOX обрывают передачу. Память сообщений — окно
CW Messages и F1..F8 в главном окне. Оживлены CAT KS/KY/ZZKM/ZZKS/ZZKY.

Настройки — вкладка Transmit, подвкладка Hardware, перед блоком FM/CTCSS.

Программный сайдтон точен для передачи текста и прямого ключа; при иамбике
таймингом владеет FPGA, поэтому там верен только аппаратный тон.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:20:56 +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 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 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 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
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 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
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 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 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 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
ew8bak 93ab03f1eb Merge branch 'main' into feature/slice-audio-input
# Conflicts:
#	ewsdr.lpi
2026-07-21 22:44:22 +03:00
ew8bakandClaude Opus 4.8 66c9fa63b9 fix(settings): профиль устройства уезжал в нулевой MAC при старте по AutoStart
Настройки персистятся секцией по MAC радио, но ResolveDevice брал MAC только из
списка найденных дискавери. У сохранённых устройств поля MAC не было вовсе, и
START по AutoStart/CONNECT без дискавери шёл с MAC 00:00:00:00:00:00 — грузился
и сохранялся чужой профиль. Пользователь видел «настройки сбросились»: выключался
wideband, менялся sample rate, терялись банды и паны, причём в зависимости от
того, нажимал ли он перед стартом DISCOVER.

- DeviceStore: у TSavedDevice поле MAC + persist в hpsdr_devices.ini (MAC=..),
  FindSavedByAddr/SetSavedMac (пишет ini только при изменении), MacIsZero.
- RadioController.ResolveDevice: saved-ветка восстанавливает и MAC.
- RadioController.EnsureDeviceMac: добор неизвестного MAC — найденное в этой
  сессии → ini → короткий служебный поиск в сети (700 мс, unicast на DirectIP,
  OnDeviceFound временно снят, чтобы не трогать список устройств в UI).
- ConnectDevice: добор MAC до Connect, после успеха MAC запоминается за
  сохранённым устройством, так что следующий AutoStart идёт без пробника.

Проверено на железе (ANAN 172.16.2.200): в hpsdr_devices.ini появился
MAC=04:91:62:FD:7B:86, секция нулевого MAC больше не создаётся, старт по
AutoStart и старт после DISCOVER дают один и тот же профиль.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 22:43:32 +03:00
ew8bakandClaude Opus 4.8 4aa180e7b2 wip(audio): выбор устройства ВВОДА на каждом слайсе + вкладки OUT/IN в AUD
Паритет с уже существующим выбором вывода: на каждом слайсе своё устройство
входа (микрофон/виртуальный кабель).

- 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>
2026-07-21 21:52:24 +03:00
ew8bakandClaude Opus 4.8 d51f0fb614 feature(calibration): частотная калибровка опорника (ppm) + TO Error в слоте QO-100
Пункт 3 плана. Ручные подгонки остаются: ppm работает ПОД ними, а не вместо —
уход тракта где-то больше, где-то меньше.

- Settings: TCalibration.FreqCalPPM (JSON cal_freq_ppm, +-100 ppm, per-device).
  Смысл знака: «опора выше номинала на N ppm», положительное значение
  поднимает показываемые частоты.
- RadioBackend: виртуальный SetFreqCalPPM.
- HPSDRNetwork: CalFreq(Hz) = Hz / (1 + ppm) в частотном слове — все четыре
  точки (главный DDC, DDC панов, feedback-DDC PureSignal, DUC передачи).
  Тактовый генератор радио менять нечем, поэтому правим запрос.
- PlutoBackend: штатный xo_correction в ad9361-phy (device-attr биндинги
  добавлены в IIOBindings). Номинал XO снимается при коннекте с округлением
  до 100 кГц, чтобы наша же коррекция с прошлой сессии не накапливалась;
  после записи LO переписываются, иначе правка ждала бы следующей перестройки.
  Одна цифра лечит RX и TX на всех диапазонах.
- QO-100: TXvtrEntry.TxLOError (JSON tx_lo_error) + поле «TO Error (Hz)» на
  странице QO-100. Сам транспондер-offset держим стандартным (8089.5 МГц),
  уход тракта правим поправкой; ActiveTXFreqHz учитывает её, подсказка внизу
  страницы считает TX-частоту уже с ней.
- UI-фиксы вкладки Calibration: длинные подсказки переведены на несколько
  строк (WordWrap — TLabel по умолчанию режется по краю группы), заголовки
  таблиц укорочены и не заезжают под кнопку «= S-meter»; RelayoutCalTab
  сбрасывает прокрутку страницы перед перекладкой (SetBounds в TScrollBox
  задаёт координаты относительно прокрученного клиента).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 12:35:46 +03:00
ew8bakandClaude Opus 4.8 a95292d2c0 feature(pluto): компенсация hardwaregain в уровнях + живое усиление в блоке RF AGC
На 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>
2026-07-21 11:50:28 +03:00
ew8bakandClaude Opus 4.8 6de8c00689 feature(rx): плавный аттенюатор 0..31 дБ в UI, память на диапазон, компенсация уровней
Аттенюатор был только в 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>
2026-07-21 11:41:21 +03:00
ew8bakandClaude Opus 4.8 366d4c9821 feature(calibration): офсет спектра/водопада на диапазон и на трансвертер
Вторая калибровочная таблица рядом с S-метром (паритет с Display Cal Offset
в Thetis): у display-тракта своё окно/детектор, и его уровень расходится с
показаниями стрелки на пару дБ, поэтому таблицы раздельные.

- Settings: DispBandDB[12] / DispXvtrDB[8] (JSON cal_disp_band_i /
  cal_disp_xvtr_i, дефолт 0 — картинка не меняется до первой правки).
- WDSPEngine: колбэк OnDispCal(PanId) — движок раз в кадр спрашивает офсет в
  UpdateSpectrum и прибавляет к пикселям (главный спектр/водопад в копирующем
  цикле, паны in-place при ненулевом офсете). На TX-дисплее не применяется.
  Так обошлись без хуков на каждое присваивание FCenterFreq.
- RadioController: общий CalLookup(Hz, BandTbl, XvtrTbl) — на нём SMeterCalFor
  и DispCalFor(PanId): главный дисплей по FCenterFreq, пан по FPanFreqHz[p],
  т.е. кросс-банд паны получают свой офсет. Колбэк вешается в CreateEngines,
  поэтому работает и в GUI, и в ewsdrd; web-зеркало получает готовые кадры.
- SettingsForm: группа «Spectrum / waterfall» с той же разбивкой (диапазоны +
  включённые трансвертеры активного железа) и кнопка «= S-meter» — копия
  таблицы стрелки как старт калибровки; общая раскладка сеток вынесена в
  LayoutCalOffsets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 11:16:34 +03:00
ew8bakandClaude Opus 4.8 797bfc2e7c feature(calibration): офсет S-метра на диапазон и на трансвертер, раздельно по железу
Вместо одного офсета на всё устройство — таблицы «на диапазон» и «на слот
трансвертера»; состав таблиц задаёт активное железо.

- 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>
2026-07-21 11:02:04 +03:00
ew8bakandClaude Fable 5 c645b0d1e6 feature(settings): вкладка Calibration — детектор мощности/КСВ, V/A, S-метр
Паритет с Thetis-Enhanced (Det. Cal. / Volts&Amps правки):
- TCalibration в TGlobalSettings (JSON cal_*): модель детектора Alex-моста,
  per-band множитель FWD/REV 0.5..2.0, множитель напряжения, Voff/Sens
  датчика тока, офсет S-метра.
- AlexDetToWatts: константы Thetis computeAlexFwdPower/computeRefPower по
  моделям (ANAN-100 / 200D / 7000DLE-Anvelina Pro 3 / 8000DLE-Orion MkII,
  6m-спецкейс REV). Дефолт = прежняя формула ANAN-100 (поведение не меняется).
- OnHPStatus: FWD и REV через модельную формулу x множитель банда;
  КСВ = (1+p)/(1-p), p=sqrt(RevW/FwdW) (Thetis) вместо отношения сырых ADC;
  напряжение x SupplyVoltCal; ток с калибруемыми Voff/Sens.
- WDSPEngine.SMeterCalDB: офсет в GetSMeterDBm/GetSliceSMeter (главный
  S-метр, slice-флаги, web).
- Выбор модели в комбо подставляет модельные дефолты датчика тока
  (Anvelina Pro 3/7000DLE: 340 мВ / 88 мВ/А — Thetis GetDefaultVoltCalibration).
- Фиксы: гард FLoading при постройке вкладки (SetItemIndex стреляет OnChange
  на программном присвоении -> AV на nil-контролах), FPageCalib/FNavCalib
  в списках SelectPage (вкладка была невидимой).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 14:35:34 +03:00
ew8bakandClaude Fable 5 7a999c95ee fix(puresignal): цель feedback 22 для Orion MkII + outlier-фильтр calcc
Разбор «IMD3 хуже, чем в Thetis»: юзерская плата (Orion MkII/Anvelina
PRO3) при стоковой цели feedback 152 перегружает входные каскады
ADC2208+кодека — фидбек сам интермодулирует, PS корректирует по
искажённому наблюдению. Замеры eu2av (Thetis-Enhanced): оптимум FB≈22
при ATT≈10 дБ.

- PSFBTarget: цель уровня feedback, авто по плате (board 5 → 22, иначе
  152), настройка ps_fb_target (0 = авто)
- авто-ATT: пороги от цели (FB > 1.5×цели / FB < 0.7×цели при ATT>0),
  ddB = 20*log10(FB/цель). Заодно чинит «ATT всегда 0»: старый порог
  >181 на этой плате никогда не достигался
- PSOutlierSigmaEff → SetPSOutlierSigma (новый экспорт libwdsp): MAD-
  фильтр выбросов перед фитом calcc, авто σ=5.0 для Orion MkII,
  настройка ps_outlier_sigma (-1 = авто)
- лампа FB в поповере: зоны от цели (0.7–1.3× зелёная), подпись
  «FB: тек/цель»

Требует libwdsp с патчами eu2av (форк wdsp, коммит d27684c).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 23:01:08 +03:00
ew8bak 6a9aebc23c fix(tx): enable protocol 2 CIC compensation filter 2026-07-17 18:51:07 +03:00
ew8bak 6197ed0aee Add flat FM RAW transmit path 2026-07-16 20:33:39 +03:00
ew8bak cd816c54d6 feat(fm): add universal FM RAW decoder output 2026-07-16 18:58:28 +03:00
ew8bakandClaude Fable 5 3acd569da5 feat(puresignal): PureSignal-предыскажение для openHPSDR P2 + кнопки PS/2TON
Протокол (по pihpsdr new_protocol.c): на PTT при PS DDC0<-ADC0 (RX-feedback),
DDC1<-ADC2 (TX DAC loopback), 192к/24бит, sync DDC1->DDC0 (byte 1363=0x02,
interleaved в потоке DDC0), частоты DDC0/1 = TX в HP-пакете, Alex0 bit18
(мост обратной связи). Caps.HasPureSignal для плат с DDCBase=2
(Angelia/Orion/Orion2/Saturn); попутно NumADCs=2 для Angelia.

WDSPEngine: PushPSFeedback (деинтерлив -> pscc(TXA, tx=DDC1, rx=DDC0) из
сетевого потока), PS-параметры calcc в кэше с переприменением после
пересоздания TXA; SetTXTwoTone (PostGen mode 1, 700/1900, 0.49+0.49,
инверсия LSB/CWL/DIGL, мьют mic как TUN).

RadioController: state machine команд (порт Thetis PSForm timer1) в
PureSignalTick @10Гц + auto-attenuate (AttOnTX по уровню feedback, цель
~152, через DUC Specific); SetPSMox и перестройка feedback-DDC на T/R;
маршрутизация DDC0 в pscc из OnDDCIQ; SetTwoTone с гейтами safety.

UI: кнопки PS (ЛКМ = поповер настроек, ПКМ = быстрый toggle) и 2TON в
блоке TX; поповер TPureSignalPopup (дочерний контрол, Wayland-safe):
PS ON/OFF, CAL, RESET, 2TON, лампа/уровень feedback, cals, corr,
GetPk/SetPk, MOX/CAL wait, PH#, Auto-atten, Relax ptol, Pin/Map/Stbl,
Tint, тона двухтональника. Персист ps_* в tx-секции per-device.

Не проверено на железе; Save/Restore коррекции не реализованы.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 11:55:26 +03:00
ew8bakandClaude Opus 4.8 28ae832b71 feat(slices): per-pan tuning wheel and per-slice RX mute on TX
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>
2026-07-14 22:35:19 +03:00