Files
ewsdr/test/tci
ew8bakandClaude Opus 5 fa38fa7f75 fix(tci): бухгалтерия пейсинга переезжала из прошлой передачи в следующую
Посреди передачи из MSHV рядом с сигналом вставала вторая несущая — и не
изредка, а с первых миллисекунд посылки. Это голая несущая гетеродина DUC:
очередь пересыхает, модуляции нет.

Цепочка. Конец посылки клиент делает командой trx:N,false. SetMOX(False)
снимает TCIMicActive внутри Invoke, а FTxClient обнуляется ПОСЛЕ возврата из
него, в HandleTrx. Опустить FTxRunning умел только тик планировщика — и только
пока видел живого FTxClient с уже снятым TCIMicActive. Окно = хвост SetMOX,
пара миллисекунд против шага тика в полкванта: тик попадал в него через раз.

Промах означал, что следующая передача идёт МИМО TxResetAccounting и наследует
FTxOwed, FTxInFlight, FTxSatSinceUs и оценку задержки. Долг сразу у потолка,
в полёте чужие кредиты прошлой посылки, условие выдачи не выполняется — маркеры
не идут вовсе, пока не сработает сторож (выдержка до 250 мс). Нулевой pre-roll
кончается раньше, очередь DUC сохнет, радио отдаёт несущую.

Лечение — инвариант «нет клиента, нет и идущей передачи», как в ClearTxClient
(он его соблюдал, а HandleTrx нет):
* FTxRunning := False рядом с FTxClient := Client — первый тик передачи всегда
  начинает с TxResetAccounting, каким бы ни был исход гонки;
* то же в обоих местах, где источник снимается;
* то же в выходе PushTxChrono по C = nil — страховка от будущих путей.

Аванс (FTxLeadKeep, FTxGapUs) не трогаем: он намеренно переживает передачу,
на нём стоит pre-roll следующего PTT.

Стенд: новая фаза «повторы» в TestTxPacing — шесть циклов PTT на одном клиенте,
проверка флага сразу по подтверждению trx:0,false и счёт маркеров в первые
200 мс каждой передачи. Негативный контроль на старом коде: флаг протухал 6/6,
маркеров 4 вместо 22. Стенд 262/262, test/cat 50/50, GUI и демон собираются.

★Отдельной тестовой процедурой это сделать нельзя: третий TRadioController в
процессе даёт AV в FreeEngines (состояние WDSP), стенд падает до проверок.
Фаза живёт внутри TestTxPacing и стартует после потерь ответов — то есть с
заведомо отравленной бухгалтерией.

★Почему дефект вылез только сейчас: TraceDump('tx') из снятых зондов стоял в
конце SetMOX(False) и писал до 16 тысяч строк — десятки миллисекунд ровно в
этом окне, и тик гарантированно успевал. Прогоны шли с EWSDR_TXTRACE=1, так
что инструмент собой же и прикрывал гонку.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
2026-08-24 18:45:55 +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.