Files
ewsdr/test/tci
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
..

Стенд 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.