Files
ewsdr/test/tci
ew8bakandClaude Opus 5 210f3e6457 fix(cw): обрыв манипуляции — поколение, а не флаг: пакет следом его отменял
Один дефект в двух местах: и 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>
2026-08-20 10:44:52 +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.