Commit Graph
100 Commits
Author SHA1 Message Date
ew8bakandClaude Opus 5 fe6fb9e292 fix(cat): ZZVB копировал VFO при опросе, KY писал в сокет без MSG_NOSIGNAL
Три замечания ревью в CAT-слое.

ZZVB. Команда была переписана под заглушку VAC (усиление приёма в
виртуальном кабеле, которого у нас нет), но заменили только комментарий —
тело осталось прежним, от старого ошибочного алиаса «обмен VFO». А ветка с
пустым суффиксом — это ФОРМА ОПРОСА: логгер, который просто спрашивает
«ZZVB;», молча затирал VFO B частотой VFO A, то есть терял сплит оператора и
даже не получал в ответ ошибку. Соседи по этому же диффу (ZZVA, ZZVG, ZZVS,
ZZAA, ZZAP, ZZBI, ZZBM, ZZDN, ZZMA, ZZMV, ZZQM, ZZOA, ZZPO, ZZSR) тело
получили, ZZVB — нет. Теперь это строчная заглушка в таблице, рядом с ZZVC и
ZZVD, которые про тот же несуществующий VAC.

ZZEB. Число полос эквалайзера принималось любое из 0..10 и уходило прямо в
TTXSettings.EQNumBands, то есть в СОХРАНЯЕМЫЙ TX-профиль. Потребители знают
ровно два случая: WDSPEngine.PushTXEQProfile ветвится на «3», редактор в
настройках — на 3 и 10. «ZZEB000…;» записывал ноль полос, всё прочее тихо
играло по полной 11-узловой кривой с чужой подписью. Принимаем только 3 и 10.

CATTcp.SendStr. Писал в сокет голым fpSend/send с флагами 0. В этой же ветке
WebUtils.SockSend получил MSG_NOSIGNAL ровно потому, что запись в закрытый
клиентом сокет иначе приходит как SIGPIPE, а он по умолчанию убивает процесс
целиком; обработчика сигнала в дереве нет. CAT про это забыли, а добавленный
здесь же цикл дозаписи расширил окно: длинный ответ (IF, ZZEB, список
режимов) уходит теперь несколькими send, и каждый может застать клиента уже
ушедшим. Пишем через WebUtils.SockSend — заодно ушла платформенная развилка.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 10:44:14 +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 66892fc7e0 fix(tci): пара «клиент+приёмник» только по факту передачи; перебор имён .part
[P1] FTxClient/FTxRx писались ДО Invoke, и команда, которая ничего не сделала,
всё равно их перебивала. Клиент, уже передающий с приёмника 1, шлёт
trx:0,true,tci — передатчик занят, SyncSetTRX не делает ничего (Started=False),
а маркеры ИДУЩЕЙ передачи с этого мига уходят под номером 0. MSHV такие блоки
отбрасывает (network.cpp:231), то есть передача просто замолкает. Теперь пара
назначается после Invoke и только при Started — тем же признаком, по которому
назначается хозяин эфира. Тот же гейт закрывает близнеца: trx:<N>,true без
',tci' поверх своей же передачи больше не снимает источник модуляции. Снятие
(',false') работает как прежде.

[P2] Имя временного файла (pid + счётчик) уникально внутри процесса, но не
между запусками: «.part», оставшийся от прошлой жизни (публиковать было
нечем), плюс повторно выданный системой pid дают EEXIST на создании — и
задание пропадало молча. TCICreateTempNear перебирает до 64 имён, но только
пока ошибка — «имя занято»: нет прав или каталога перебором не лечится.

Стенд test/tci: 219 проверок (было 217). Новое — занятое имя «.part» записи не
теряет (стенд занимает ровно то имя, которое возьмёт писатель) и пустая
команда TRX не меняет номер приёмника в маркерах. Негативный контроль на обе
правки. Попутно в тесте поправлены два комментария, описывавшие прежнюю
реализацию публикации.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:02:40 +03:00
ew8bakandClaude Opus 5 ecd42c32f8 fix(tci): передача с слайса — маркер TX_CHRONO под номером клиента, фронт rfTransmitting
Живой прогон с MSHV на втором слайсе (openHPSDR): приём в порядке, эфир по
trx:1,true,tci поднимается, а звук от клиента не доходит. Два независимых
дефекта, оба видны только на приёмнике > 0 — на rx0 передача работала, потому
стенд их и не ловил.

1. Маркеры TX_CHRONO уходили с receiver = 0 жёстко. MSHV шлёт TX-аудио ТОЛЬКО
   в ответ на маркер и фильтрует все входящие бинарные блоки по номеру
   приёмника первой же строкой обработчика (network.cpp: `if
   (pStream->receiver != tci_trx) return;`, ветка TxChrono там же и собирает
   блок). У клиента на слайсе tci_trx = 1, так что маркеры отбрасывались
   целиком. Теперь вместе с клиентом-модулятором запоминается номер приёмника
   из его же TRX (FTxRx), и маркеры идут под ним.

2. Changed(rfTransmitting) из SetTxSlice стирал TCIMicRequested. Контроллер
   шлёт это поле и просто как «перерисуй TX-бейджи», а адаптер понимал любой
   такой сигнал при FTransmitting = false как «передача кончилась». Приходил
   он посередине нашей же команды: SyncSetTRX ставит просьбу → RequestSliceTx
   → SetTxSlice → Changed → просьба стёрта → SetMOX выбирает микрофон уже без
   неё. В эфир шёл микрофон оператора (тишина), а TX-аудио клиента
   отбрасывалось — TCIMicActive не поднят. Теперь ловится фронт «было → стало»
   (FLastTxOn), а не всякое уведомление.

Стенд test/tci: 217 проверок (было 214), все зелёные. Новое — часть E, слайс
как приёмник 1: по trx:1,true,tci модуляция из TCI взята, маркеры TX_CHRONO
идут и названы номером 1. Негативный контроль разделён: каждая правка краснит
свою проверку.

Проверено на железе: MSHV на втором слайсе передаёт.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 21:47:30 +03:00
ew8bakandClaude Opus 5 3dc7035f9e fix(tci): писатель WAV — один поток с очередью, файл публикуется атомарно
SAVE запускал поток на каждую запись с FreeOnTerminate: его никто не держал
и никто не ждал. Замерено отдельным процессом — при штатном выходе сразу
после сохранения от ожидаемых 100000044 байт на диске оставалось 40960, а
заголовок заявлял полную длину; на медленном каталоге идущие подряд SAVE
плодили сотни потоков, чья память стеков в 128-МБ бюджет не входила.

Теперь писатель один и принадлежит адаптеру (лениво на первом SAVE), очередь
ограничена TCI_RECORD_MAX_JOBS = 16 (сверх — клиенту writer busy и возврат
резерва), а деструктор адаптера гасит его через TCIStopWriter: Close →
WaitDrained (без срока) → Free. Срока здесь нет намеренно: поток, стоящий в
write/fsync, изнутри процесса не останавливается (Terminate не указ, Free
обязан WaitFor, бросить живой TThread нельзя — он ходит в общий бюджет),
поэтому срок не ограничивал выход, а только терял подтверждённые клиенту
записи. Ограниченный выход = писатель отдельным процессом, одним TThread не
делается; это записано в коде и в доке.

Результат записи больше не игнорируется: FileWrite возвращает число байт и
при ошибке даёт 0/-1 без исключения, поэтому на полном диске файл спокойно
дописывался до конца огрызком. TCIWriteAll — цикл с проверкой каждого вызова,
плюс FileFlush перед публикацией (на ext4 с отложенным размещением ENOSPC
приходит именно там).

Файл появляется под целевым именем целиком или не появляется вовсе: данные
пишутся во временный файл рядом (эксклюзивно, не по симлинку), а публикует
их TCIPublishFile — renameat2(RENAME_NOREPLACE) напрямую через Do_SysCall,
если его нет — link + unlink, если нет и ссылок (FAT/exFAT, часть CIFS/SMB и
FUSE) — отказ с сохранением данных в .part. FileExists + rename не делается
нигде: это тот самый TOCTOU. На Windows — MoveFileW без REPLACE_EXISTING.

doc/TCI.md: §2.5 переписан (писатель-очередь, остановка, публикация); заодно
исправлено устаревшее описание склейки кусков в Take (её нет с 7aae0fd).

Стенд test/tci: 214 проверок (было 199), все зелёные. Новое — временный файл
убирается после удачи, неудачная запись не оставляет ни файла, ни .part, за
всё время записи 32 МБ целевое имя ни разу не видно незаконченным, отказ
сверх потолка заданий, после Close заданий не берут, TCIStopWriter дожидается
и самой записи, и хвоста очереди за ней. Все новые гарантии прогнаны
негативным контролем; путь link проверен сборкой с выключенным renameat2,
сам renameat2 — под strace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 19:40:31 +03:00
ew8bakandClaude Opus 5 7aae0fdfcd fix(tci): SAVE отдаёт писателю сами куски записи, а не сплошную копию
Потолок 128 МБ всё ещё пробивался примерно вдвое на пике: Take собирал
линейную копию ДО освобождения FChunks, поэтому при полном бюджете рядом
жили ~128 МБ кусков и ~128 МБ копии, а счётчик показывал 128. Передача
резерва писателю (9b77809) закрывала учёт, но не сам пик.

Копии больше нет вовсе. Take отдаёт куски КАК ЕСТЬ — новый TTCIRecTake
(Chunks + Chunk + Count + Reserved), — и вместе с ними уезжает их место в
бюджете целиком. TTCIWavWriter пишет куски в файл подряд: в WAV сэмплы и
так лежат встык, а хвост последнего куска за Count просто не наш. Место
отпускается в ReleaseData, ровно один раз на любом пути выхода Execute.

Продовый путь теперь не выделяет под запись ни одного лишнего байта:
TTCIPcm остался только внутри куска. Склейка нужна одному стенду, чтобы
проверять порядок и уровень, — она и живёт в стенде (FlatTake).

Стенд 199/199: 2.5 с записи отдаются ТРЕМЯ кусками по секунде (свёрнутая
копия дала бы один), счёт бюджета при Take не меняется, резерва хватает
на отданные куски, склеенные куски дают непрерывный звук, писатель
возвращает резерв по окончании. Проверено, что копирующая реализация Take
краснит четыре проверки. GUI (--ws=qt6) и демон зелёные.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 13:18:20 +03:00
ew8bakandClaude Opus 5 9b77809a73 fix(tci,cat): резерв бюджета переезжает писателю; строгий разбор полей ZZ-команд
Три замечания по 79132f1.

1. [P2] Лимит памяти рекордеров обходился при SAVE. Take собирал сплошную
   копию записи, а DropData тут же возвращал исходные куски в бюджет —
   копия жила дальше в асинхронном TTCIWavWriter уже неучтённой. Один SAVE
   поднимал настоящее потребление примерно вдвое, а на медленном или
   зависшем сетевом каталоге очередь writer-потоков и их буферов росла
   мимо потолка вовсе. Теперь копия НАСЛЕДУЕТ резерв кусков, из которых
   собрана: Take отдаёт его out-параметром Reserved, писатель держит до
   конца записи и отпускает в ReleaseData — ровно один раз, сколько бы
   путей выхода ни было у Execute. Новых денег у бюджета копия не берёт,
   так что потолок теперь считает и очередь сохранений тоже.

2. [P2] Пять ZZ-команд проверяли длину поля, но не содержимое:
   StrToIntDef(s, 0) превращал любую нечисловую пару символов в индекс 0.
   ZZBSxx; переключал диапазон на нулевой вместо ?;, ZZBMxx; и
   ZZAUxx;/ZZBPxx; двигали VFO, ZZFIxx; выбирал фильтр 0. Разбор приведён
   к идиоме ZZFL/ZZFH: TryStrToInt, иначе ошибка формата.

3. [P3] Сообщение об отказе запуска TCI звало в Settings → Advanced, а
   настройки там уже не живут — вкладка CAT (переезд был в de0830f).

Стенд 194/194: Take отдаёт резерв размером с копию, после Take занят ровно
он, писатель возвращает его по окончании. Без фикса первая проверка
краснеет. GUI (--ws=qt6) и демон зелёные. doc/TCI.md §2.5 и сводка стенда,
doc/CAT_STATUS.md — таблица разбора.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:50:21 +03:00
ew8bakandClaude Opus 5 79132f133c fix(tci): рекордер линейного выхода — память под потолком, файл только в своём каталоге
Два дефекта уровня P1 в LINE_OUT_RECORDER_*. Авторизации в протоколе нет
(§3.1), bind наружу разрешён — значит «сколько стоит одна строка из сети»
это вопрос живучести процесса, а не аккуратности.

1. Неограниченное выделение памяти. CmdRecorder проверял только ValidRx
   (номер в потолке), а конструктор выделял буфер целиком: 300 с × 48 кГц
   × 2 канала × int16 = 57.6 МБ на команду, приёмников 1+MAX_SLICES=7, то
   есть 403 МБ семью строками. У мёртвого приёмника Feed не зовут — значит
   срок записи никто не проверял; уход клиента рекордеры не трогал вовсе;
   DropDeadRxStreams бежит только на rfDevice/rfConnected и смене карты
   слайсов, в покое не срабатывает. Память жила до остановки сервера.

   Лечение тремя замками:
   - память набирается кусками по секунде, START не стоит ни байта;
     куски не перевыделяются (никакого realloc в DSP-потоке) и склеиваются
     один раз в Take — уже после того, как рекордер вынут из таблицы;
   - общий бюджет TCI_RECORD_MAX_BYTES (128 МБ) на все рекордеры сразу,
     спрашивается на каждый кусок; отказ не рушит запись, набранное
     остаётся сохраняемым;
   - освобождение по трём событиям: START требует живого приёмника
     (RxActive, ответ receiver is not running), тик сервера подметает
     истёкшие окна по часам (SweepRecorders — окно закрывается от START
     и без единого блока звука), уход клиента забирает его записи
     (DropClientRecorders; рекордер живёт на приёмнике, но платит за него
     тот, кто нажал START).

2. Перезапись произвольного файла. Путь из сети уходил в fmCreate почти
   как пришёл — вместе с '..' и абсолютными путями. Теперь TCIRecordPath
   берёт из строки ТОЛЬКО имя файла, каталог — настроенный tci.record_dir
   (пусто = <каталог конфигурации>/records). Каталог из просьбы
   отбрасывается молча: полный путь на сервере клиенту всё равно
   бесполезен, файл ложится не на его машину. Имя валидируется (пусто,
   '.', '..', управляющие, ':', длиннее 120, расширение не .wav →
   bad file name); после ExtractFileName выйти за каталог нечем. Файл
   создаётся эксклюзивно (TCICreateNewFile: O_EXCL|O_NOFOLLOW на Unix,
   CREATE_NEW на Windows) — ни перезаписи, ни симлинка, без окна между
   FileExists и созданием; клиенту заранее file exists.

Попутно: MainForm.ApplyTCISettings собирал TTCISettings по полям с
чистого листа — новое поле RecordDir обнулялось бы при каждом применении
вкладки CAT. В uses TCIStreams Windows стоит первым намеренно: иначе его
TCriticalSection перекрыл бы SyncObjs (та же грабля, что в DX-кластере).

Стенд 189/189 (было 169): 11 проверок имени файла (/etc/passwd.wav,
../../.., D|\rec\a.wav), бюджет (START не выделяет, растёт кусками,
потолок, отказ не рушит запись, срок истекает без Feed), существующий WAV
не перезаписывается, START на мёртвом приёмнике и с чужим номером, и
сквозная проверка в части E — клиент стартует запись, набирает память
живым звуком через WDSP, рвёт TCP, бюджет возвращается к нулю. Без фиксов
новые проверки краснеют. GUI (--ws=qt6) и демон зелёные.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:30:36 +03:00
ew8bakandClaude Opus 5 adbb8d02ac fix(tci): Stream.length — вещественные отсчёты всего блока, а не на канал
MSHV не декодировал FT4 по TCI (по виртуальному кабелю — декодировал).
Регрессия из dc6f996: там length аудио был переведён на «сэмплы на канал»
по формулировке §4.3, а живые клиенты считают по нему БАЙТЫ блока
(network.cpp: `int cr2 = pStream->length*bit_s;` с шагом `chan*bit_s`,
на передаче `quint32 cr3 = pStream->length*bit_s;`). При Channels=2
(умолчание MSHV) клиент разбирал половину каждого блока: звук с дырами
50%, водопад шире и грязнее, декодер разваливался. В моно дефекта не
видно — единицы совпадают, поэтому первый прогон стенда увёл в сторону.

Правка верна и по документу, а не только по клиенту: §3.4 после IQ
(«количество вещественных отсчётов… комплексных = length/channels»)
говорит «аудиопоток приёмника ПОЛНОСТЬЮ ПОВТОРЯЕТ IQ поток» и
перечисляет ровно три отличия — каналы, формат сэмплов, число сэмплов в
пакете. Единиц length среди них нет, поле в struct Stream одно.
Развилка по типу потока была вычитана из воздуха.

§4.3 путает две величины, и её формулировка верна лишь для моно:
AUDIO_STREAM_SAMPLES — кадры НА КАНАЛ, Stream.length — отсчёты ВСЕГО
блока. Что arg1 считает кадры, видно из самой §4.3 дважды: минимум
512/256/128/100 на 48/24/12/8 кГц даёт обещанные «не меньше 10 мс»
только при счёте на канал, и потолок data[16384] = 2048 × 2 × float32.
TX_CHRONO замыкает круг: клиент шлёт столько отсчётов, сколько названо
в length маркера, и возвращает то же число обратно.

Размер блока не менялся (2048@48к = 42.7 мс). Гипотеза про клиппинг
тапа RX_AUDIO проверена замером и снята: пик 0.115.

Стенд: проверки length переписаны на инвариант «байт = length × размер
отсчёта»; новый test/tci/ft4_bench.py снимает поток TCI и PipeWire-
источник одновременно, режет на нарезки FT4 по общим часам и гоняет
через настоящий jt9 --ft4 (блок разбирает КАК MSHV — этим и поймал).
Проверено вживую: MSHV декодирует FT4 по TCI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:26:29 +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
ew8bak 92c01fa2c6 Merge feature/cat-tx-cw: CAT-команды TX-тракта и телеграфа, ревизия алиасов и подписей, починка транспортов 2026-08-17 13:15:14 +03:00
ew8bakandClaude Opus 5 20bf3c1f39 fix(cat): транспорты — короткая запись в TCP и молчаливый отказ serial-порта
CATTcp.SendStr делал один send на ответ. TCP не обязан отдавать весь буфер за
раз, а усечение здесь не ошибка — длинный ответ (IF, ZZEB, список режимов) мог
уехать обрезанным, и молча. Дописываем остаток в цикле.

CATSerial помечал порт активным ДО SerOpen, а открытие шло внутри потока: при
отказе порт навсегда оставался «работающим» в ActiveCount и UI, а причина
нигде не оседала. Открытие переехало в TCATSerialPort.Start и делается
синхронно, так что отказ виден сразу — FActive остаётся False, причина в новом
свойстве LastError. Поток теперь только читает, закрывает владелец в Stop.

Заодно Andromeda-порт назначался по галке в настройках, без оглядки на то,
поднялся ли порт: HasAndromeda рапортовал о панели, которой нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 13:13:46 +03:00
ew8bakandClaude Opus 5 27a8d1d5cf feat(cat): TX-тракт и телеграф; ревизия алиасов, подписей и валидации
Со времён прошлой правки CAT в EWSDR появились TX-профили со всей «звуковой»
цепью и телеграф — команды, которые doc/CAT_STATUS.md объявлял незакрываемыми,
стали рабочими.

TX-тракт: MG/ZZMG (усиление микрофона), MO/ZZMO + ZZTM (монитор передачи и его
уровень), ZZTL/ZZTH (кромки TX-фильтра), PR/ZZPK + ZZPL (речевой компрессор),
ZZET + ZZEB (эквалайзер), ZZTO + ZZTU (мощность и кнопка настройки), ZZUT
(2TON), ZZLI + ZZUS (PureSignal), ZZTP (выбор профиля), ZZFD (девиация).

Телеграф (были проброшены только KS/KY/ZZKS/ZZKY): ZZCS скорость, ZZCL тон,
ZZCI иамбик, ZZCB/ZZCD break-in и hang-time, ZZCM сайдтон.

Ревизия подписей. Комментарии у заглушек писались по буквам кода, а не по
Thetis, и врали примерно в 150 местах. Хуже: часть РАБОТАЮЩИХ команд была
привязана не к своей функции — внешний софт получал осмысленный, но неверный
ответ, что хуже честной заглушки. Всё сверено с CATCommands.cs:

  ZZMA  режим        -> кнопка MUT            ZZNN  заглушка -> SNB
  ZZRX  переход в RX -> аттенюатор RX1        ZZNS  SNB      -> кнопка NR2
  ZZRV  версия ПО    -> напряжение питания    ZZVS  алиас ZZSP -> операции с VFO
  ZZMV  индекс режима-> счётчик памяти        ZZBM  режим    -> VFO B вниз на nn
  ZZKM  режим кейера -> запуск CW-макроса     ZZFT  всегда A -> TX-частота (split)
  ZZFI/ZZBS  были алиасами -> сами держат фильтр и диапазон

Ошибочные алиасы на громкость/мощность сняты (ZZAA ZZVG ZZOA ZZAP ZZDN ZZAC
ZZBI ZZMB ZZVA ZZPD ZZPO ZZQM ZZSR ZZRD ZZRU) — теперь честные заглушки с
указанием, где функция живёт на самом деле.

Кенвудовские команды сверены отдельно, покрытие полное (все 40 из Thetis),
ширины полей совпадают с CATStructs.xml. Исправлено: SM отдавал 4 цифры вместо
5; RD/RU перестраивали VFO, хотя это RIT; KY не срезал набивку поля пробелами
и гнал её в эфир паузами; CT принимала любой символ и «CT9;» молча гасил тон;
OF/OS несли реализацию сами, а ZZOT/ZZOS были заглушками — клиент Thetis
обращается как раз к ZZ* и не получал ничего.

Валидация. Команды без параметров не проверяли суффикс: «TXanything;» доходил
до CmdTX и ПОДНИМАЛ ПЕРЕДАЧУ (так же RX UP DN BD BU QI RC ID IF) — эталон
отбраковывает лишний суффикс в парсере, у нас теперь список в IsParamless.
ZZTX поднимал передачу на любом значении кроме нуля («2=TUNE» — выдумка).
ZZFL/ZZFH принимали поле любой длины от 4 символов и через StrToIntDef молча
схлопывали кромку в ноль. ZZMG принимал 1-2 символа. KY/ZZKY не ограничивали
текст 25 символами, и очередь передачи могла расти произвольно.

Осознанные отклонения от эталона сведены в отдельную таблицу документа:
ZZBS (индекс диапазона вместо кода), ZZMN (имя режима вместо пресетов
фильтров), ZZST (шаг FM вместо размера шага настройки), ZZCD (потолок 2000 мс —
наш предел, он же в поле HangDelay пакета DUC Specific).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 13:13:33 +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
ew8bak f89f202a34 Merge feature/dxcluster-spots: споты DX-кластера на панадаптерах, мода по бэндплану, кнопка DX в RX 2026-08-17 11:29:04 +03:00
ew8bakandClaude Opus 5 6ed1be3dc7 fix(dxcluster): резолв имени, гейт логина, TTL и мелочи UI по итогам ревизии
Сеть и резолв (DXClusterClient):
- Резолв ушёл в отдельный поток: системный резолвер блокирующий и не
  прерывается, а сокета в этот момент ещё нет — Stop из UI-потока висел на
  DNS-таймауте. Сессия ждёт квантами по 200 мс, просыпаясь на FStopEvent;
  Stop теперь отрабатывает за 100-200 мс в любой фазе.
- Реестр запросов: по одному резолверу на имя, не больше DX_MAX_RESOLVERS.
  Не дождавшись, сессия оставляет запрос в реестре и на следующей попытке
  цепляется к нему же (иначе зависший резолвер плодил бы вечные потоки).
  Слотов несколько, чтобы смена адреса работала поверх зависшего прежнего;
  литеральный IP разбирается до реестра — ввод адреса руками обязан работать
  всегда. Запись живёт по счётчику ссылок, исключение в резолвере не
  оставляет слот занятым.
- getaddrinfo вместо netdb.ResolveHostByName: тот ходит в DNS сам и
  /etc/hosts не читает вовсе (getent находит localhost, ResolveHostByName —
  нет), т.е. локальный алиас кластера не работал. Заодно реентерабельно.
- WSAStartup перенесён в initialization, WSACleanup убран: Stop не ждёт
  резолвер, а тот может сидеть в gethostbyname.

Логин (DXClusterClient):
- Команды пользователя больше не уходят в незавершённый логин: очередь
  разбирается только после post-login, SendCommand говорит в лог, что
  команда ждёт.
- ONLINE не по таймеру, а по существу: LoginSettled требует ответа сервера
  после учётки (с потолком молчания), пароль ждёт своего приглашения.
  Подтверждение ставится ПОСЛЕ разбора куска, а не на приход байтов —
  иначе исход логина зависел от границ TCP-пакетов.
- Приглашения и отказы: строгий детектор (текст, заканчивающийся
  двоеточием) и для отправки пароля, и для вердикта — по вхождению слова
  пароль улетал командой от строки приветствия. Повтор приглашения пароля
  или позывного = отказ авторизации (dxsError, без реконнекта); опоздавшее
  приглашение после слепой отправки позывного отказом не считается.
  Эхо уже отвеченного приглашения гасится окном в одну строку.
- Ошибка отправки post-login рвёт сессию, а не только цикл команд;
  неотправленная очередь возвращается на следующее соединение.

UI и данные:
- QSY по споту крутит активный VFO, а не всегда A (MainForm).
- TTL спотов чистится тиком независимо от видимости оверлея (DXSpotStore
  .Purge + ServiceDXCluster).
- Выделение в окне списка держится по позывному И частоте: один позывной
  живёт на разных диапазонах (DXClusterForm).
- Кнопка DX правит и отложенную копию настроек, иначе debounce SETUP
  возвращал прежнее состояние подписей (MainForm).

Проверено на фейковом кластере: приглашения с CRLF и без, границы
TCP-пакетов, отказ по паролю и по позывному, опоздавшее приглашение,
ловушки ложного срабатывания.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 22:00:41 +03:00
ew8bakandClaude Opus 5 eb4af61fad ui(spectrum): полоса главного фильтра — как у слайсов, со своим оттенком
Главный фильтр рисовался иначе, чем слайсовые полосы, причём в CPU-пути
сильнее, чем в GL.

1. Порядок отрисовки. Кромки, несущая и её треугольник у главного шли ПОСЛЕ
   кривой спектра, у слайсов — до неё, а в GL-вьюхе весь блок фильтров идёт
   перед DrawSpectrumCurve. Из-за этого на CPU главная полоса единственная
   лезла поверх сигнала и выглядела жирнее и ярче слайсовых при тех же цвете и
   толщине линий. Блок перенесён к слайсам, до кривой: порядок кадра теперь
   одинаков у обоих путей.

2. Заливка. Была НЕПРОЗРАЧНОЙ, рядом с просвечивающими слайсовыми смотрелась
   плотным блоком. Теперь та же полупрозрачность, что у слайсов; прозрачность
   вынесена в общие SPEC_BAND_ALPHA/SPEC_BAND_ALPHA_TX (VfoOverlay, рядом с
   палитрой слайсов) — раньше это были четыре магических числа по двум вьюхам.
   Осиротевшая FillBandRaw удалена.

3. Цвет. С полупрозрачностью полоса перестала читаться: SpecFilter (34,78,106)
   почти совпадает с нижним цветом фонового градиента спектра (31,79,108), и
   заливка давала не оттенок, а затемнение — над низом градиента выходило
   ровно (32,79,108), то есть фон. Полосе дан свой цвет темы SpecFilterBand:
   тёплый янтарь RGB(255,150,40) на тёмной (фон сине-стальной, тёплое на нём
   читается) и насыщённый зелёный RGB(60,150,60) на светлой. SpecFilter
   остался подложкой подписей AGC — иначе перекрасились бы и они.

4. Слайс B перекрашен из оранжевого в васильковый RGB(97,118,255): с янтарной
   полосой главного фильтра оранжевый стал неразличим. Оттенок выбран по
   свободному месту в круге: заняты 0°(TX), 31°(полоса), 55°(E), 132°(A),
   190°(C), 195°(G), 276°(F), 308°(D); самый широкий промежуток 195..276°,
   середина ≈236°. Палитра одна на всё, поэтому вместе с полосой и несущей
   перекрашиваются буква слайса на спектре и бейдж во флаге VfoOverlay.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На экране не проверено: нужен эфир со
слайсами; цвета правятся одной строкой (SpecFilterBand в AppTheme,
SLICE_COLORS в VfoOverlay).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 23:14:59 +03:00
ew8bakandClaude Opus 5 d0c82ade73 ui(spectrum): вертикаль несущей не перечёркивает букву полосы фильтра
Буква слайса стоит по центру полосы фильтра у самого верха, и вертикаль
несущей шла туда же — от Y=0. В SSB/CW это не мешало (несущая лежит у кромки
полосы, буква — по центру), а в модуляциях с несущей (AM/FM/DSB) центр полосы
и есть несущая, и линия шла прямо по букве.

Одна функция на оба вида — CarrierTopY (CPU) / CarrierTopYGL (GL): смотрит,
попадает ли вертикаль под глиф, и если да, отдаёт Y ПОД буквой. Решение по
факту пересечения, а не по списку «модуляций с несущей»: список пришлось бы
вести и сопровождать, а в SSB зазор и так выходит нулевым сам собой.

- Слайсы: DrawSliceFilterLinesRaw и DrawSliceFilterMarkers.
- Главный VFO: вместе с линией вниз уезжает и её «шляпка» — треугольник в CPU
  (у RawTriangleDown появился параметр верхней координаты) и метка в GL. Без
  этого они остались бы поверх буквы и зазор не помог бы.
- Полоса TX при split сверяется с той же буквой — она нарисована на RX-полосе.
- В GL буква главного флага теперь рисуется ПОСЛЕ линий, как в CPU: порядок
  наложения у путей должен совпадать.

Кегль буквы вынесен в BAND_LETTER_FONT: рисование и расчёт зазора обязаны
брать его из одного места, иначе зазор разъедется с глифом.

Метрика проверена замером (Qt6, offscreen): глиф 8 bold = 7x12 px ⇒ вертикаль
стартует с Y=14, по горизонтали зазор ±6 px от центра полосы. Правится
константами CLEAR_X/CLEAR_Y рядом с функцией.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На экране не проверено: нужен эфир с
AM/FM и слайсами.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 19:21:08 +03:00
ew8bakandClaude Opus 5 13b46f0b7d ui(dxcluster): системный шрифт вместо захардкоженного 'Courier New'
По проекту гарнитуры не задаём — работает системный шрифт (907c2c0 выпилил
'Courier New' из flat-контролов, 19babef из статус-бара). Новый код DX-кластера
это правило нарушал в трёх местах, все три введены в a2fdc3a.

- DXSpotOverlay: подписи спотов на спектре рисуются системным шрифтом —
  гарнитура убрана совсем. Моноширинность там ни к чему: позывные не в
  колонках, ширина чипа и так меряется по TextWidth. Размер по-прежнему задан
  через Font.Height от высоты ряда — иначе подпись не влезала бы при DPI.
- DXClusterForm: список спотов и лог соединения ОСТАЮТСЯ моноширинными
  осознанно (колонки частота/позывной/мода/UTC/спотер выровнены пробелами в
  FormatSpotLine, пропорциональный шрифт их развалит), но имя теперь
  платформенное — MONO_FONT = Consolas/Monospace, как в CWTerminalForm и в
  полосе CW у PanafallPanel. 'Courier New' на Linux не существует вовсе,
  fontconfig молча подменял его.

Font.Size/Font.Style не трогал: их проект оставляет и после перехода на
системный шрифт (в StatusBar размер сохранён).

Дифы всех четырёх коммитов ветки проверены по Font.Name — других мест, где
DX-код задаёт гарнитуру, нет. Сборка ewsdr (--ws=qt6) — ОК.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 18:43:36 +03:00
ew8bakandClaude Opus 5 84f710476f fix(dxcluster): сборка под win64 — Windows.pas перекрывал TCriticalSection
Сборщик под win64 падал в конструкторе клиента кластера: «identifier idents no
member Create». Windows.pas объявляет TCriticalSection как ЗАПИСЬ (алиас
TRTLCriticalSection), а в uses он стоял ПОСЛЕ SyncObjs — и поле FLock, и
TCriticalSection.Create разрешались в запись. На Linux эта ветка uses не
компилируется вовсе, поэтому баг жил с первого коммита DX-кластера (a2fdc3a)
и всплыл на первой же сборке под Windows.

Windows.pas убран из uses совсем: сокетам он не нужен, всё даёт WinSock2 —
так же сделано в WsClient (там тоже FLock: TCriticalSection) и HPSDRNetwork.
Причина подтверждена минимальным репро, дающим дословно тот же текст ошибки.

Сверено по исходнику winsock2.pp (FPC 3.2.2), чтобы следующая итерация
сборщика не встала на следующем имени: connect/socket/select/getsockopt/
setsockopt/FD_SET/FD_ZERO/WSAStartup/WSACleanup/inet_addr/gethostbyname/htons,
типы TWSAData/TSockAddrIn/TFDSet/TTimeVal/PHostEnt и константы INADDR_NONE/
SOL_SOCKET/SO_ERROR/SO_KEEPALIVE/WSAE* — все на месте. Указательная форма
connect(@Addr, …) тоже корректна: в winsock2 есть обе перегрузки.

Заодно FNV-хэш подписи набора спотов обёрнут в {$Q-}{$R-}: он живёт
переполнением, а проверки правятся в .lpi (в Release их сейчас нет).
Порядок «Windows после SyncObjs» проверен по всему проекту — больше нигде.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК, тест бэндплана зелёный. Win64 проверит
сборщик.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 18:32:59 +03:00
ew8bakandClaude Opus 5 d12d8239da feat(dxcluster): мода спота по бэндплану, когда комментарий молчит
В CW и SSB спотеры сплошь и рядом не пишут моду, и такой спот оставался
dxmUnknown: ни фильтр по модам его не видел, ни QSY по нему моду не ставил.
Теперь порядок такой: сначала комментарий (как было), и только если он молчит —
участок бэндплана.

DXSpotStore.DXModeFromFreq — таблица участков, читается сверху вниз, первое
попадание выигрывает. Узкие «водопои» цифры стоят РАНЬШЕ широких сегментов:
FT8 на 7074 живёт посреди телефонного участка R1, FT4 на 21140 — посреди
15-метрового, и без такого порядка они утонули бы в SSB.

★ Границы — по IARU Region 1 (наш регион), а споты прилетают со всего мира,
поэтому там, где регионы расходятся, мода НЕ выводится вовсе: пропуск честнее
ошибки. Отсюда дырки 1843-1850 (R1 телефон против R2 CW), 60 м (канальный),
маячные щели, 2 м/70 см кроме FT8-окон. Два спорных куска всё же отданы R1:
3570-3600 — цифре (у R2 это ещё CW, но споты там почти сплошь FT8/RTTY) и
7053-7300 — телефону (у R2 ниже 7125 данные).

QO-100 (даунлинк 10489.5-10490.0) — отдельной веткой по плану AMSAT-DL, теми же
границами, что рисует BandPlanOverlay: CW, NB/DIGI, две SSB-зоны. Маяки и
mixed modes не гадаем.

Угаданное помечено (TDXSpot.ModeGuessed) и в окне списка выводится с '?' —
'CW?' против 'CW': спотер моду не называл, а на границах участков таблица
может ошибаться, поэтому разницу видно. На спектре в чипе только позывной —
там ничего не изменилось.

Проверено консольным тестом на этих же юнитах: 25 частот + 6 полных строк
кластера (комментарий перебивает таблицу; 7012.5 → CW guessed, 7145 → SSB
guessed, 10489.680 → SSB guessed; 1846, 60 м и 144.300 остаются без моды).
Сборка ewsdr (--ws=qt6) и ewsdrd — ОК.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 18:13:28 +03:00
ew8bakandClaude Opus 5 17fa965aec feat(dxcluster): споты на всех панадаптерах + кнопка DX в блоке RX
Подписи спотов рисовались только на главном пане. Теперь их видит каждый пан,
в чьё окно попадает частота спота.

Оверлей — ЭКЗЕМПЛЯР НА ПАН, а не один общий: кэш полосы подписей ключуется
центром/спаном/шириной, а у панов они свои — общий оверлей пересобирался бы на
каждый пан каждый кадр, то есть ровно то, ради чего кэш и заводился. Живёт в
TPanafallPanel (AttachDXSpots/DetachDXSpots/DXOverlay, Owner=панель), база
спотов по-прежнему одна на всех. FDXSpotOverlay в MainForm стал алиасом на
оверлей пана 0 — как FSpecView для FPan.View: настройки, тема и тик затухания
идут общим циклом по FPans, и главный пан перестал быть особым случаем (иначе
тумблер DX гасил бы подписи только на нём).

★ Version стора глобальна, а окно у пана своё, поэтому в EnsureRendered на
смену версии сначала берётся снимок окна и сверяется его подпись (FNV-1a по
позывному, частоте, моде и метке времени). Совпала — ни пересборки битмапа, ни
перезаливки GL-текстуры: спот, севший на чужой диапазон, до этого пана не
доходит. Без этого цена пересборки множилась бы на число панов. Снимок берётся
один раз и переиспользуется раскладкой — стор второй раз не дёргаем.

Клик по подписи на пане N = QSY слайса ЭТОГО пана (активного, если он здесь,
иначе первого; нет ни одного — создаём в точке) с модой по комментарию
кластера и полосой пресета этой моды. Своего VFO у панов N нет, а ретюнить их
DDC под спот нельзя — увезло бы весь пан. Мода спота → режим вынесена в общую
DXSpotRadioMode для обоих путей.

Низкий пан (грид): полоса подписей и штрихи пропускаются целиком — раньше в
GL-пути полоса легла бы поверх спектра, а штрихи пошли бы снизу вверх.

Кнопка DX переехала из тулбара в блок RX левой панели, сразу после CTUN (ряд
стал четырёхколоночным: CTUN | DX | Channel | BEACON): споты — часть приёмного
вида, а не глобальная команда уровня DISCOVER/START/SETUP. Заодно снят расчёт
SafeLeft, резервировавший под неё место в шапке.

Подписи по умолчанию ВЫКЛЮЧЕНЫ (show_spots: дефолт и фолбэк чтения были True),
состояние переживает перезапуск. Клик по кнопке подливает состояние в открытый
SETUP: там своя копия конфига, и первая же правка любого поля страницы вернула
бы подписи обратно.

fix: Stop клиента кластера больше не держит главный поток. Он делал Terminate +
WaitFor, а поток в этот момент сидит в блокирующем recv и просыпается лишь по
своему кванту (до 1 с), на застрявшем send — до SO_SNDTIMEO. Всё это время
окно висело на выходе и на переподключении после правки настроек. Теперь Stop
рвёт живой сокет SockShutdown (хэндл — зеркало под тем же локом, поток снимает
его ДО close, поэтому shutdown по закрытому fd невозможен), а FormDestroy зовёт
неблокирующий RequestStop первой строкой: поток доживает параллельно с
разборкой радио, и join в конце уже никого не ждёт.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На железе не проверено: доп. паны
требуют подключённого радио.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 18:00:08 +03:00
ew8bakandClaude Opus 5 a2fdc3a741 feat(dxcluster): споты DX-кластера на панадаптере
Telnet-клиент DX-кластера, база спотов и подписи позывных прямо на спектре
на своих частотах. Разложено на три слоя, как бэндплан и лупа маяка.

DXClusterClient.pas — один рабочий поток: резолв, неблокирующий connect с
select квантами по 200 мс (Stop не ждёт таймаут соединения), логин позывным,
чтение строк, реконнект с backoff. Приглашения логина И пароля ловятся в
незавершённом хвосте буфера — типичный telnet-prompt приходит без CR/LF.
Ошибки recv отличаются от таймаута кванта (EAGAIN/EINTR/WSAETIMEDOUT), иначе
на ECONNRESET поток крутился бы в пустом цикле вместо реконнекта. Отправка
дописывает частичный send. LCL-free.

DXSpotStore.pas — потокобезопасная база: дедуп по позывному, TTL, потолок
записей, монотонный Version. Единственная точка обмена потока с UI: никакого
Synchronize, UI сам замечает правки по Version, как оверлеи — по ключам кэша.

DXSpotOverlay.pas — рендер по модели BandPlanOverlay/VfoOverlay: кэшируется
только полоса подписей (W × BandH) в key-color битмап, пересборка строго по
dirty-ключу, на кадр — один keyed-композит. Штрихи от полосы до низа спектра
рисует вызывающая сторона теми же примитивами, что и прочие маркеры: CPU —
RawVLine внутри RawBegin/RawEnd, GL — DrawLine по готовому списку X/цвет.
GL берёт тот же битмап текстурой и заливает её только при смене RenderVersion.
Пересекающиеся подписи раскладываются лесенкой, цвет гаснет с возрастом,
свой позывной выделен; палитра парная под тёмную и светлую тему.

DXClusterForm.pas — окно списка: споты, лог соединения, строка команды
кластеру (диалект set/filter у всех свой — не угадываем). Двойной клик или
Enter = QSY. Данные тянутся поллингом по Version/LogVersion.

Интеграция: кнопка DX в тулбаре (ЛКМ — подписи на спектре, ПКМ — окно),
клик по подписи спота = QSY с автовыбором моды по комментарию кластера,
страница SETUP → DX Cluster с персистом в секции "dxcluster". Правки SETUP
прилетают посимвольно, поэтому запись конфига, TTL стора и переподключение
откладываются до паузы в наборе — иначе набор позывного стоил бы шесть
реконнектов, а промежуточный TTL «3» необратимо выбросил бы споты.

Частота спота кладётся как есть и сравнивается с GetViewWindow: на QO-100
кластеры постят downlink 10489.xxx, что совпадает со шкалой пана само собой.

Побочно в общих юнитах: WebUtils.SockSetRcvTimeout, FlatMemo.OnChange,
FlatListBox.OnKeyDown, BlendBitmapKey вынесен в interface VfoOverlay (одна
копия дворд-блендера на проект).

Проверено на локальном фейковом кластере: логин и пароль по prompt без CR/LF,
разбор спотов (включая QO-100), уход в RETRY по RST, Stop за 200 мс на
висящем connect. На железе рендер не проверялся.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:47:26 +03:00
ew8bak ab848404b1 Merge feature/beacon-zoom-panel: лупа наведения на маяк QO-100
Окно Beacon получило карточку BEACON TUNING: узкий кусок спектра вокруг маяка в
высоком разрешении плюс мини-водопад. На спектрограмме 576к маяк занимает пару
пикселей и ткнуть в него для наведения декодера почти нельзя — здесь то же
наведение делается по широкому следу. Наведение с главной спектрограммы не
тронуто.

Увеличение делает сам WDSP: второй analyzer на том же IQ главного тракта, со
своей FFT и ассиметричным span-clip, посчитанным прямо из границ окна в герцах.

Вошло:
- лупа с кликом-наведением, зумом колесом, панорамированием и FOLLOW;
- автопоиск маяка (AUTO) по min-hold — критерий «непрерывный след», а не
  «самый громкий»;
- состояние захвата прямо в лупе: полоса приёма меняет начертание на локе,
  рядом SNR и расхождение с опорной частотой;
- окно лупы переживает перезапуск (сдвиг от опорной, а не абсолют);
- контур больше не принимает за маяк голую несущую — проверка BPSK по
  констелляции;
- взведённый клик снимается при любом наведении, не только мышью;
- попутно: пропадавшая расстройка тона в статусе терминала телеграфа ('%+d'
  в Format Object Pascal не работает).

На эфире проверено частично: наведение и картинка — да, автопоиск и новый
критерий лока — нет.
2026-08-14 13:03:27 +03:00
ew8bakandClaude Opus 5 c30c23a64b fix(beacon): взведённый клик снимается при любом наведении, не только мышью
FBeaconArming гасился только в обработчике клика по спектру. Навёл AUTO из окна
лупы — флаг остался взведён, и следующий случайный ЛКМ по панораме утаскивал
маяк на пустое место. Те же грабли были для наведения из web и CAT.

Снимаем в обработчике rfBeaconLock, как только BeaconDecodeFreqHz стал
ненулевым — кто навёл, роли не играет. FSpectrumDirty просим только в этот
момент: маркеры и так едут вместе с кадрами, а на стопе спектр статичен, дёргать
перерисовку 10 раз в секунду незачем.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 13:01:42 +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 504265f8ad fix(cw): в статусе терминала пропадала расстройка тона
Format(', tone %+d Hz', ...) печатал ', tone d Hz' — без значения. Флага '+' в
Format Object Pascal нет, синтаксис здесь %[индекс:][-][ширина][.точность]тип;
встретив '%+', FPC съедает процент с плюсом и копирует остаток спецификатора
как обычный текст. Знак ставим руками, отрицательные %d печатает со своим
минусом сам.

Найдено попутно — тот же промах был в новом коде лупы маяка.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:13:33 +03:00
ew8bakandClaude Opus 5 a5daf33cc9 feat(beacon): состояние захвата видно прямо в лупе
Чтобы понять «попал или нет», приходилось переводить взгляд на плитки метрик
под графиком. Теперь это читается в самой лупе:

- Полоса приёма показывает захват начертанием: в поиске три линии пунктирные,
  на локе — сплошные и стянуты сверху скобой. Новых цветов не добавлено, та же
  семантика $0020D0FF, что и на главном спектре.
- Строка захвата под частотой центра: LOCK · 14.2 dB (зелёная) / SEARCH (янтарь)
  / BEACON OFF. Состояние берётся из BeaconState — это гистерезисный
  FBeaconLocked, которому доверяет сам контур коррекции, а не мгновенный
  CarrierLock: тот мерцал бы на границе.
- Δ от опорной рядом: расхождение измеренной частоты маяка с опорной, то есть
  расстояние между оранжевым и зелёным маркерами числом. Это невязка, которую
  лок стекает в LOError; в пределах дедбэнда (20 Гц) — зелёная, иначе янтарная.

Знак у Δ ставится вручную: флага '+' в Format Object Pascal нет (это printf'изм),
'%+.0f' молча съедается и в строку попадает голый хвост спецификатора — на
экране было «Δ .0f Hz» вместо значения.

Состояние и SNR снимаются раз в тик в PollZoom и кладутся в поля: копия
TBeaconScope в отрисовку не лезет, а SNR сравнивается с допуском 0.2 дБ —
иначе дрожание в сотых долях дБ пересобирало бы кадр каждый тик и dirty-флаг
потерял бы смысл.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:13:33 +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 ac216fa332 fix(beacon): трасса лупы не рисовалась до первого клика по маяку
Перерисовку спектра лупы гейтил не тот сигнал: возврат GetBeaconZoomSpectrum
(«в кэше лежит непрочитанный кадр») игнорировался — функция звалась как
процедура, — а FZoomDirty ставился только по смене границ окна и по сдвигу
маркеров. Пока никто не панорамирует, границы не меняются, а до клика все три
маркерные частоты нулевые и неподвижные: кадр собирался один раз и застывал.

Клик задаёт наведение декодера, дальше BeaconDecodeFreqHz непрерывно едет за
несущей в ServiceBeaconLock — сравнение с FLastDecHz начинает срабатывать
каждый тик, и спектр «оживает». Водопад работал с самого начала: он всегда шёл
по своему честному флагу свежести, хотя слой тот же самый и анализатор один.

Теперь свежесть кадра — основной триггер (трасса идёт 15/с, как и даёт
анализатор); проверки окна и маркеров оставлены как дополнительные, они дают
отклик на зум/пан не дожидаясь следующего кадра.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:54: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 b0a93ddb14 Merge feature/cw: телеграф (кейер прошивки + программный), терминал и декодер CW
Телеграф на openHPSDR P2 и Pluto/LibreSDR: кейер в FPGA (тайминги в железе) и
программный кейер для бэкендов без него, pitch как ЕДИНЫЙ сдвиг гетеродина в
движке, сайдтон, CWX/F1..F8, CAT KY/KS/ZZKM, терминал с набором с клавиатуры и
декодером приёма. Голос в CW заблокирован, реле T/R и антенна ходят за ключом
прошивки, запрет передачи (RX-only XVTR, DoNotTx) действует и на кейер FPGA.

Попутно: окно повторов Specific-пакетов только со старта (e87ca6b), правки в
трансвертере больше не травят память КВ-диапазона (827902f), перестройка
слайса по CAT внутри полосы захвата обновляет флаг (00860e5).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:47:57 +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 e87ca6b186 fix(hpsdr): окно повторов Specific-пакетов — только со старта, не на каждую пере-сборку
RebuildDDCSpecific сбрасывал FResendCount, а этим счётчиком keepalive-поток
управляет окном повторов: пока он меньше 100, каждые 500 мс переотправляются
DDC Specific и DUC Specific. Окно задумано как стартовый костыль — порты
1025/1026 на железе открываются только после Run=1.

Сброс из пере-сборки делал окно рецидивирующим: любая переконфигурация на
живой сессии заводила ещё десять пере-латчей DDC Specific в течение пяти
секунд. Вызывают её вход и выход PureSignal из передачи (то есть каждый MOX,
когда PS вооружён), добавление и удаление панадаптера, смена sample rate.
Пакет конфигурирует все DDC разом, поэтому пере-латч на ходу трогает и
главный приёмник — этой же операцией когда-то был получен мусорный поток на
доп. панах. С PS и 2TON выходило по двадцать пере-латчей на передачу, что
похоже на давний открытый пункт про всплески водопада при 2TON.

Счётчик сбрасывается теперь только по Run=1. Размен: повторы заодно
маскировали потерю UDP-пакета с конфигом посреди сессии, и переконфигурация
стала одним пакетом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 13:55:19 +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
ew8bak 0ec120c2d5 Merge feature/tx-profiles: TX-профили, подвкладки Transmit, заводские EQ ESSB, привязка профиля к «флаг + модуляция» + пласт UI/DPI 2026-08-09 22:06:15 +03:00
ew8bakandClaude Opus 5 6195c80ce3 fix(tx): полоса ЧМ на передаче — занимаемая полоса, а не кромки bp0
TXFilterEdgesHz была тонкой обёрткой над TXSignedEdges, то есть отдавала
в отрисовку кромки аудио-полосовика перед модулятором. У SSB/CW/DIGI/AM
это то же самое, что полоса вокруг несущей, а у ЧМ — нет: FM RAW рисовался
как (20..7000), WFM как (20..15000), то есть полоса уезжала вверх от
несущей и визуально «переключалась на USB».

Теперь у экранных кромок своя семантика — Карсон ±(девиация + верхний срез
аудио) для FM/WFM/FM RAW, остальные режимы как были. TXSignedEdges и
SetTXABandpassFreqs не тронуты: bp0 остаётся портом Thetis, в эфире ничего
не меняется. Попутно 20.0/7000.0 из трёх мест FM RAW сведены в
RAW_AF_LOW/RAW_AF_HIGH.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 19:09:31 +03:00
ew8bak 7c9064659b ui(main): align left panel sliders 2026-08-05 16:51:43 +03:00
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