Последняя невыполненная команда протокола. Ключ к ней в том, что arg3 — длительность интервала, который ТОЛЬКО ЧТО кончился, а не начинающегося. Документ задаёт это алгоритмом: первое нажатие keyer:0,true,0, отпускание keyer:0,false,142 («посылка длилась 142 мс»), следующее нажатие keyer:0,true,58 («пауза длилась 58 мс»). Отсюда перевод: тип элемента — это состояние ключа ДО фронта, то есть обратное пришедшему; arg3 = 0 играть нечего. Почему не дёргать ключ по приходу пакета: приход говорит, что интервал кончился, а не сколько он длился, и манипуляция «по приходу» — это сетевой джиттер прямо в эфир, тот самый «пьяный матрос», ради которого третий аргумент в протоколе и появился. Новый TCWElemPlayer (CWMorse.pas) держит очередь элементов и играет их подряд по абсолютным дедлайнам: сумма длительностей равна времени у клиента, значит отставание постоянно (сеть + один элемент) и не накапливается. Очередь опустела — ключ отпускается и следующая пачка начинается с чистого дедлайна: элементы чередуются, искажения нет, зато оборвавшийся клиент не оставляет в эфире несущую. Контроллер: CWKeyerElement(Mark, Ms) ставит элемент в очередь, CWElemKey раздаёт фронты — чужая манипуляция это прямой ключ с точными длительностями, поэтому у Pluto она идёт во вход прямого ключа локального генератора (он сам поднимает сессию, рисует огибающую и сайдтон), а у openHPSDR в бит CWX прошивки (тем же путём идёт передача текста). Гейт CWTXActive, как у CWXSend; обрыв общий с текстом — касание манипулятора, снятие MOX и уход из телеграфа гасят чужую манипуляцию тем же CWXAbort. Передачу KEYER не поднимает: при break-in PTT даёт прошивка (или сессия генератора), без него оператор держит MOX сам. Адаптер: номер передатчика разбирается как у TRX (bad receiver / receiver is not running), захват §3.5 общий с TRX — передатчик один, и ключ держит тот же, кто держит эфир. Паузы обрезаются TCI_KEYER_GAP_MAX_MS = 1 с (пауза целиком прибавляется к отставанию от клиента, а дольше секунды — это «оператор задумался», и честнее догнать реальное время), посылки — 5 с. Стенд test/tci: 238 проверок (было 219). Новая часть C2 меряет ДЛИТЕЛЬНОСТИ по фронтам ключа (посылка 150 / пауза 60 / посылка 150, допуск 30 мс), проверяет отпускание на пустой очереди, старт следующей пачки без «догона» дедлайна, обрыв и нулевую длительность; в части D — разбор аргументов команды и сквозная проверка, что keyer:0,true,<мс> ключ не замыкает (это пауза), а keyer:0,false,<мс> замыкает. Негативный контроль на инверсию перевода. ★Локальный генератор вооружается только при живом устройстве, поэтому сквозная проверка подставляет FDevConnected/FRunning на время. doc/TCI.md: §2.6 описывает команду целиком, §3.1 и §4 переписаны под то, что из пары KEYER/TX_FOOTSWITCH остался только второй; попутно убран устаревший абзац §2.3 про «потоки — этап 2». На железе с настоящим ключом по сети ещё не гонялось. 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.