Один дефект в двух местах: и TCWElemPlayer (чужая манипуляция по TCI), и TCWSender (передача текста — F1..F8, набор в терминале, CAT KY) сбрасывали FAbortReq прямо в Enqueue. Поток замечает обрыв только на очередном срезе Hold, то есть в пределах 5 мс; всё, что прилетело в это окно, снимало флаг, и уже отданный AbortPlay/AbortSending пропадал бесследно. Чем это кончается в эфире: обрыв существует ровно затем, чтобы касание манипулятора, снятие MOX или уход из телеграфа прекратили программную манипуляцию (как «key hit» в Thetis/pihpsdr). Проглоченный обрыв означает, что оператор взял ключ в руки, а софт продолжает манипулировать вместе с ним. Источники, кормящие Enqueue, ровно такие, чтобы в 5 мс попадать: клиент TCI шлёт элементы KEYER десятками в секунду; терминал отдаёт набор В ЭФИР ПО СИМВОЛУ на нажатие клавиши; поле текста KY — 25 знаков, поэтому длинное сообщение логгер шлёт несколькими командами подряд. У передачи текста цена выше: AbortSending чистит FPending, но взятое сообщение живёт в локальной переменной потока, и доигрывается ВЕСЬ его остаток — замер на стенде дал 620 мс (остаток 720-мс тире на 5 WPM), у KEYER — 742 мс. Лечение общее: вместо флага счётчик поколений. AbortPlay/AbortSending его инкрементируют, Enqueue не трогает вовсе, поток несёт своё поколение (Take, Aborted, Hold, SendChar) и принимает новое только сам — когда отпустил ключ и бросил очередь. У TCWSender есть точка получше: текст и поколение берутся одним заходом под лок (TakeText(out Gen)), а это закрывает заодно и вторую точку сброса — ту, что стояла в начале Execute. Добор по ходу передачи стал TakeMore(Gen): после обрыва не берём ничего, и набранное ПОСЛЕ него не теряется вместе с брошенным, а уходит следующим сообщением со своим поколением. Стенд: 244 проверки (было 238). В C2 — регрессия на KEYER, новая часть C3 на передачу текста (раньше TCWSender стендом не покрывался вовсе): длительность точки по скорости, старт сообщения, обрыв с пакетом следом, и что после обрыва передача снова идёт. Обе регрессии проверены негативным контролем — на старом CWMorse.pas они падают с теми самыми 742 и 620 мс. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Стенд 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.