RX_AUDIO по TCI снимается ДО громкости и мьюта, поэтому замьюченный слайс обязан продолжать считаться, пока его кто-то качает по сети — иначе клиент остаётся без звука ровно потому, что оператор убрал его у себя в комнате. Гейт в ProcessSlicesFor это и делал, но спрашивал ГЛОБАЛЬНЫЙ флаг «тапы аудио установлены», а его взводит SetTaps(True) на старте сервера. То есть галка «Enable TCI server» одна, без единого клиента, заставляла гонять полный fexchange0 по каждому молчащему слайсу — до шести штук, ~47 блоков в секунду на каждый, плюс два входа в критические секции на блок в DSP-потоке. Клиент, качающий один слайс из шести или вообще только IQ, оплачивал остальные так же. Флаг стал ПОСЛАЙСОВЫМ (TSliceChan.TapWanted, SetSliceTapWanted). Кто его выставляет — владелец потоков: TTCIAdapter.PushTapWants полностью пересчитывает картину по FStreams и раскладывает по слотам. Считаются только потоки RX_AUDIO: LINE_OUT и рекордер снимаются ПОСЛЕ мьюта, и на молчащем слайсе им достаётся тишина независимо от того, посчитали его или нет. Пересчёт зовётся отовсюду, где множество потоков или карта слайсов меняются: StartStream (синхронно и ДО ответа клиенту — тогда не теряется ни один блок), StopStream, DropClientStreams, DropDeadRxStreams, StopAllStreams и OnState по MapChanged — слот мог сменить хозяина, и разрешение обязано переехать вместе с ним. Список слушателей снимается под FStreamLock, и лок отпускается ДО похода в движок: DSP-поток входит в тап, уже держа FSliceLock движка, и берёт в нём FStreamLock — обратный порядок дал бы клин. Глобальные FAudioTapsActive/SetAudioTapsActive/PushAudioTapsActive удалены. FAudioTapCount остаётся: он по-прежнему гейтит сам вызов тапов (FireAudioTap) и сборку звука DMR. Из AttachEngineCallbacks флаг не восстанавливается намеренно — слайсы пересозданный движок не наследует вовсе. Заодно: StopStream завершал перебор через Exit прямо из-под try, минуя хвост процедуры, — заменено на Break. Стенд (+3 проверки, негативный контроль в обе стороны): мьют слайса не глушит RX_AUDIO по TCI; без потока молчащий слайс не считается вовсе (наблюдатель — обычный аудио-тап, который теперь права требовать демодуляции не имеет); попросили поток снова — слайс снова в работе. 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.