feat(tci): KEYER — чужой ключ очередью элементов, а не сетевыми фронтами

Последняя невыполненная команда протокола. Ключ к ней в том, что 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>
This commit is contained in:
2026-08-19 22:46:37 +03:00
co-authored by Claude Opus 5
parent 66892fc7e0
commit 84b90b23c0
6 changed files with 598 additions and 26 deletions
+7
View File
@@ -58,6 +58,13 @@ const
TCI_TX_BUFFERING_MIN = 50;
TCI_TX_BUFFERING_MAX = 500;
TCI_RECORD_MAX_SEC = 300; // потолок записи линейного выхода
// Потолки длительностей у KEYER (§4.3). Посылка длиннее пяти секунд — это
// уже не телеграф, а залипший ключ; паузу же обрезаем куда жёстче: она
// прибавляется к отставанию проигрывателя от клиента, а всё, что дольше
// секунды, — это «оператор задумался», и после такой паузы честнее
// догнать реальное время, чем тащить его дальше.
TCI_KEYER_MARK_MAX_MS = 5000;
TCI_KEYER_GAP_MAX_MS = 1000;
// ★Общий потолок памяти ВСЕХ рекордеров сразу. Приёмников у нас
// 1 + MAX_SLICES, и предельные 300 с на каждом — это 57.6 МБ × 7 ≈ 403 МБ,
// которые неавторизованный клиент выпрашивал бы семью строками. Бюджет