Files
ew8bakandClaude Opus 5 fe3d862d00 fix(dsp): канал TXA жил на два потока без разделения
TX-DSP поток зовёт fexchange0(TXA_CHAN) через буферы FTXIn/FTXOut, и ровно туда
же на каждом T/R лезет SetTXRun из потока контроллера. Гейт FTXActive их не
разделял: поток проверяет его СНАРУЖИ, а внутрь блока входит позже. Три дефекта
одной природы:

* деструктор звал SetTXRun(False), который поток НЕ останавливает — он лишь
  опускает гейт и тут же сам сливает канал своими fexchange0, то есть лезет в
  TXA одновременно с ещё живым потоком, прямо перед сносом объекта. Теперь
  Destroy сразу зовёт Close: тот опускает гейт, будит поток, дожидается WaitFor
  и лишь потом закрывает каналы. Комментарий «останавливаем TX поток» врал;
* слив на отпускании PTT шёл без всякой синхронизации — окно гонки примерно
  1-1.5% на каждый T/R (блок ~150 мкс против периода 10.7 мс). Появился FTXLock
  по идиоме RX-стороны (FMainGated + FMainLock): поток берёт лок вокруг
  ProcessTXBlock и ПЕРЕЧИТЫВАЕТ гейт под ним, оба слива в SetTXRun идут под тем
  же локом. Ждать контроллеру не дольше одного блока TXA;
* ветка ЗАПУСКА оставалась открытой: SetMOX не отсекает MOX=True поверх уже
  идущей передачи (прислать повтор вправе CAT, web и TCI), а SetTXRun(True)
  обнулял индексы mic-кольца и дёргал SetChannelState мимо лока. ★Худший исход
  не теоретический: TX-поток дописывает свой локальный tail поверх обнулённого,
  кольцо выглядит почти полным СТАРЫХ сэмплов, и они уходят в эфир пачкой.
  Теперь `if Run and FTXActive then Exit` плюс весь старт одной транзакцией под
  FTXLock, FTXActive публикуется последним.

★Перенастройку TXA в ChangeSampleRate под лок НЕ берём: там SetChannelState с
dmode=1 ждёт фейда, а фейд прокручивают fexchange0 самого потока — под локом это
клин на ~106 мс (тот самый старый фриз TX→RX). Синхронизация там своя:
остановка канала. Второй известный кросс-поточный доступ к TXA — pscc из
сетевого потока (PureSignal), так же устроено в Thetis.

Попутно закрыт use-after-free: TX-поток создаёт SetTXRun, которому открытый
движок не нужен, а гасил его только код ЗА гейтом `if not FInitialized then
Exit` в Close — на неоткрытом движке поток переживал Free объекта. Ловилось не
там, где сделано: следующий TThread.Create в процессе падал с AV (в стенде — на
секции часов после пейсинга, раньше — на третьем TRadioController в FreeEngines).
Остановка потока перенесена в начало Close, до гейта.

Стенд: порядок секций теперь намеренный — часы идут ПОСЛЕ тяжёлых частей и
служат сторожем починки потока (упадёт снова — увидим там). Новая проверка
повторного MOX с ★негативным контролем: без гейта «было 3000, стало 0».
Для неё в движке появилось диагностическое свойство TXMicFill. 276/276,
test/cat 50/50, GUI (--ws=qt6) и демон собираются.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
2026-08-24 21:20:38 +03:00
..

Стенд TCI

Прогон:

test/tci/run.sh

Собирает tcitest.pas и запускает его; код возврата ненулевой, если хоть одна проверка провалена. Внешних библиотек стенд не требует — WebSocket-клиент в нём написан на сыром сокете. libwdsp нужна только последней части (сквозной прогон через движок); без неё эта часть сообщает о пропуске, а остальные идут как обычно.

Что проверяется — в шапке tcitest.pas; чего стенд не покрывает — в doc/TCI.md, §5.

Две грабли, из-за которых стенд собирается именно так (обе стоили по часу):

  • -Mobjfpc обязателен. С -Mdelphi в командной строке выключаются вложенные комментарии, и {$MODE Delphi} внутри шапки WebUtils.pas закрывает комментарий раньше времени — компиляция падает на «illegal character».
  • Каталог .ppu — свой, с именем не lib, и путь абсолютный. Относительное lib/<cpu>-<os> компилятор ищет и относительно -Fu, то есть находит units GUI-сборки в корне проекта, а там PlatformUtils собран с LCL: стенд падает на линковке с undefined reference to TC_$FORMS_$$_SCREEN. По той же причине сборка идёт с -dHEADLESS, как у демона.

Каталоги units/ и bin/ — выход сборки, в git не попадают.

Проверка на живом эфире без передачи

python3 test/tci/live_rx_test.py --host 127.0.0.1 --port 40001

Наглядный UI-прогон с автоматическим возвратом к исходным значениям:

python3 -u test/tci/live_rx_test.py --visible --commands-only --dwell 1.5

Стенд проверяет receive-only команды, IQ, аудио демодулятора и line-out для всех обнаруженных приёмников. Он никогда не посылает START, STOP, установку TRX/TUNE, TX-аудио и CW-текст. Если радио уже на передаче, прогон аварийно прекращается.

Стенд «как MSHV»

test/tci/mshv_sim.py --rx 0            # в MSHV это «TCI Client rx1»
test/tci/mshv_sim.py --rx 1 --audio    # «TCI Client rx2»

mshv_sim.py повторяет логику TCI-клиента MSHV по его исходникам — включая обе особенности, из-за которых он ведёт себя не так, как наш стенд:

  • фильтр по номеру приёмникаif (ls2.at(0)!=tci_trx) continue;: строка, адресованная не его приёмнику, для MSHV не существует (и то же самое для бинарных блоков, по полю receiver заголовка);
  • инициализация — один запрос и один ответ: он шлёт vfo:<rx>,0; раз в 500 мс, максимум пять раз, и ждёт vfo:<rx>,0,<частота>. Не дождался — «Error To Initialize TCI Server». Других причин у этой ошибки нет.

Поэтому стенд отвечает на вопрос «почему MSHV не подключается» одной строкой, а заодно ловит то, что иначе проявляется молча: без tx_enable со своим номером приёмника у MSHV не работает PTT (set_ptt начинается с if (!tci_tx_enable) return;).

По умолчанию стенд безопасен на приём: ни TRX, ни TUNE, ни START/STOP, ни TX-аудио он не шлёт. Передачу включает явный --tx.