mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
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:
+179
-1
@@ -28,7 +28,7 @@ program tcitest;
|
||||
uses
|
||||
cthreads, Classes, SysUtils, Math, SyncObjs, Sockets, BaseUnix,
|
||||
TCIProtocol, TCIStreams, TCIServer, TCIAdapter, RadioController,
|
||||
WsClient, WebUtils, WDSPEngine, Settings;
|
||||
WsClient, WebUtils, WDSPEngine, Settings, CWMorse;
|
||||
|
||||
var
|
||||
Passed, Failed: Integer;
|
||||
@@ -1005,6 +1005,123 @@ begin
|
||||
Big := nil;
|
||||
end;
|
||||
|
||||
{ ═══════════════════════════════════════════════════════════════════════════
|
||||
C2. Чужая манипуляция (KEYER, §4.3)
|
||||
═══════════════════════════════════════════════════════════════════════════ }
|
||||
|
||||
type
|
||||
{ Приёмник фронтов ключа: пишет, ЧТО и КОГДА пришло. }
|
||||
TKeyLog = class
|
||||
public
|
||||
Down: array[0..63] of Boolean;
|
||||
At: array[0..63] of QWord;
|
||||
Count: Integer;
|
||||
T0: QWord;
|
||||
procedure Key(D: Boolean);
|
||||
end;
|
||||
|
||||
procedure TKeyLog.Key(D: Boolean);
|
||||
begin
|
||||
if Count > High(Down) then Exit;
|
||||
if Count = 0 then T0 := GetTickCount64;
|
||||
Down[Count] := D;
|
||||
At[Count] := GetTickCount64 - T0;
|
||||
Inc(Count);
|
||||
end;
|
||||
|
||||
procedure TestKeyPlayer;
|
||||
// ★Смысл проигрывателя: длительности приходят ГОТОВЫМИ (клиент замерил свой
|
||||
// ключ), и в эфир они обязаны лечь такими же, сколько бы ни болталась сеть.
|
||||
// Поэтому проверяем не «дёрнулся ключ», а ДЛИНЫ интервалов между фронтами.
|
||||
var
|
||||
Log: TKeyLog;
|
||||
P: TCWElemPlayer;
|
||||
Ev: TCWKeyEvent;
|
||||
i, Waited, Bad: Integer;
|
||||
D1, D2, D3: Int64;
|
||||
begin
|
||||
WriteLn('C2. Чужая манипуляция (KEYER)');
|
||||
Log := TKeyLog.Create;
|
||||
Ev := Log.Key;
|
||||
P := TCWElemPlayer.Create(Ev);
|
||||
try
|
||||
// Посылка 150 мс, пауза 60, посылка 150 — «точка-тире» чужим ключом.
|
||||
P.Enqueue(True, 150);
|
||||
P.Enqueue(False, 60);
|
||||
P.Enqueue(True, 150);
|
||||
Waited := 0;
|
||||
while (P.Busy or (P.Pending > 0)) and (Waited < 3000) do
|
||||
begin
|
||||
Sleep(10);
|
||||
Inc(Waited, 10);
|
||||
end;
|
||||
Sleep(50);
|
||||
Check('KEYER: фронтов ровно четыре', Log.Count = 4, IntToStr(Log.Count));
|
||||
if Log.Count = 4 then
|
||||
begin
|
||||
Check('KEYER: чередование вниз-вверх-вниз-вверх',
|
||||
Log.Down[0] and (not Log.Down[1]) and Log.Down[2] and
|
||||
(not Log.Down[3]));
|
||||
D1 := Int64(Log.At[1]) - Int64(Log.At[0]);
|
||||
D2 := Int64(Log.At[2]) - Int64(Log.At[1]);
|
||||
D3 := Int64(Log.At[3]) - Int64(Log.At[2]);
|
||||
// Допуск на планировщик — тот же порядок, что у дробного сна (5 мс).
|
||||
Check('KEYER: длительность посылки сохранена',
|
||||
(Abs(D1 - 150) < 30) and (Abs(D3 - 150) < 30),
|
||||
Format('%d/%d', [D1, D3]));
|
||||
Check('KEYER: длительность паузы сохранена', Abs(D2 - 60) < 30,
|
||||
IntToStr(D2));
|
||||
end
|
||||
else
|
||||
begin
|
||||
Check('KEYER: чередование вниз-вверх-вниз-вверх', False);
|
||||
Check('KEYER: длительность посылки сохранена', False);
|
||||
Check('KEYER: длительность паузы сохранена', False);
|
||||
end;
|
||||
// ★Очередь опустела — ключ ОТПУЩЕН. Иначе оборвавшийся клиент оставил бы в
|
||||
// эфире несущую, снять которую некому.
|
||||
Check('KEYER: на пустой очереди ключ отпущен',
|
||||
(Log.Count > 0) and (not Log.Down[Log.Count - 1]));
|
||||
|
||||
// Пачка после простоя начинается сразу, а не «догоняет» старый дедлайн.
|
||||
Log.Count := 0;
|
||||
Sleep(300);
|
||||
P.Enqueue(True, 100);
|
||||
Waited := 0;
|
||||
while (Log.Count < 2) and (Waited < 2000) do
|
||||
begin
|
||||
Sleep(10);
|
||||
Inc(Waited, 10);
|
||||
end;
|
||||
Check('KEYER: следующая пачка играется целиком',
|
||||
(Log.Count = 2) and (Abs(Int64(Log.At[1]) - Int64(Log.At[0]) - 100) < 30),
|
||||
IntToStr(Log.Count));
|
||||
|
||||
// Обрыв: очередь бросается, ключ отпускается.
|
||||
Log.Count := 0;
|
||||
for i := 0 to 9 do P.Enqueue(True, 400);
|
||||
Sleep(60);
|
||||
P.AbortPlay;
|
||||
Sleep(120);
|
||||
Bad := Log.Count;
|
||||
Sleep(300);
|
||||
Check('KEYER: обрыв гасит очередь', Log.Count = Bad, IntToStr(Log.Count - Bad));
|
||||
Check('KEYER: после обрыва ключ отпущен',
|
||||
(Log.Count > 0) and (not Log.Down[Log.Count - 1]));
|
||||
// Нулевые и отрицательные длительности не элементы: первое нажатие клиента
|
||||
// (keyer:0,true,0) не должно порождать ни одного фронта.
|
||||
Log.Count := 0;
|
||||
P.Enqueue(True, 0);
|
||||
P.Enqueue(False, -5);
|
||||
Sleep(150);
|
||||
Check('KEYER: нулевая длительность ничего не играет', Log.Count = 0,
|
||||
IntToStr(Log.Count));
|
||||
finally
|
||||
P.Free;
|
||||
Log.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ ═══════════════════════════════════════════════════════════════════════════
|
||||
D. Живой сервер: команды потоков
|
||||
═══════════════════════════════════════════════════════════════════════════ }
|
||||
@@ -1257,6 +1374,7 @@ var
|
||||
Cfg: TTCISettings;
|
||||
C, C2: TRawClient;
|
||||
S: string;
|
||||
WasConn, WasRun: Boolean;
|
||||
Blk: array[0..1023] of Byte;
|
||||
H: TTCIStreamHeader;
|
||||
Op: Byte;
|
||||
@@ -1464,6 +1582,65 @@ begin
|
||||
Check('tune:9 → ошибка', Pos('bad receiver', S) > 0, S);
|
||||
Check('tune:9 не включил TUN', not Ctrl.FTuning);
|
||||
|
||||
// ── KEYER (§4.3): чужой ключ ────────────────────────────────────────
|
||||
// Номер передатчика разбирается так же, как у TRX: командой из сети в эфир
|
||||
// по несуществующему приёмнику не уходят.
|
||||
C.SendText('keyer:9,true,0;');
|
||||
S := C.WaitText('tci_error', 1500);
|
||||
Check('keyer:9 → ошибка', Pos('bad receiver', S) > 0, S);
|
||||
C.SendText('keyer:abc,true,0;');
|
||||
S := C.WaitText('tci_error', 1500);
|
||||
Check('keyer:abc → ошибка', Pos('bad receiver', S) > 0, S);
|
||||
C.SendText('keyer:1,true,0;');
|
||||
S := C.WaitText('tci_error', 1500);
|
||||
Check('keyer на незапущенный приёмник → ошибка',
|
||||
(Pos('not running', S) > 0) or (Pos('bad receiver', S) > 0), S);
|
||||
C.SendText('keyer:0,maybe,0;');
|
||||
S := C.WaitText('tci_error', 1500);
|
||||
Check('keyer с нечисловым состоянием → ошибка', Pos('bad state', S) > 0, S);
|
||||
// Первое нажатие (arg3 = 0) — не ошибка и не звук: играть ещё нечего.
|
||||
C.SendText('keyer:0,true,0;');
|
||||
S := C.WaitText('tci_error', 400);
|
||||
Check('keyer:0,true,0 не ошибка', S = '', S);
|
||||
Check('keyer:0,true,0 не поднял манипуляцию', not Ctrl.CWXBusy);
|
||||
|
||||
// ★Сквозная проверка перевода: keyer:0,false,150 значит «посылка длилась
|
||||
// 150 мс», и она обязана дойти до манипуляции. Без телеграфного режима
|
||||
// трогать ключ нечем (CWTXActive), поэтому режим ставим сами.
|
||||
Ctrl.SetMode(MODE_CWU);
|
||||
if not Ctrl.CWTXActive then
|
||||
WriteLn(' .. keyer: манипуляция недоступна (CWTXActive=false), пропуск')
|
||||
else
|
||||
begin
|
||||
// ★Локальный генератор вооружается только при живом устройстве
|
||||
// (FDevConnected/FRunning — без радио манипулировать некуда), а радио на
|
||||
// стенде нет. Подставляем эти два флага на время проверки: нас интересует
|
||||
// перевод «пришло false ⇒ кончилась ПОСЫЛКА», а не излучающая часть.
|
||||
WasConn := Ctrl.FDevConnected;
|
||||
WasRun := Ctrl.FRunning;
|
||||
Ctrl.FDevConnected := True;
|
||||
Ctrl.FRunning := True;
|
||||
// Первое нажатие открывает передачу, второе сообщение говорит «кончилась
|
||||
// ПАУЗА 300 мс» — ключ при этом замыкаться не должен.
|
||||
C.SendText('keyer:0,true,0;');
|
||||
C.SendText('keyer:0,true,300;');
|
||||
C.Pump(120);
|
||||
Check('keyer: манипуляция пошла', Ctrl.CWXBusy);
|
||||
Check('keyer: пауза ключ не замыкает', not Ctrl.FCWLocalTX);
|
||||
// «Кончилась ПОСЫЛКА 200 мс» — вот она и есть звук в эфире. Ждём конца
|
||||
// паузы и смотрим в середину посылки.
|
||||
C.SendText('keyer:0,false,200;');
|
||||
C.Pump(250);
|
||||
Check('keyer: посылка замыкает ключ', Ctrl.FCWLocalTX);
|
||||
Ctrl.CWXAbort;
|
||||
C.Pump(150);
|
||||
Check('keyer: обрыв гасит манипуляцию', not Ctrl.CWXBusy);
|
||||
Ctrl.FDevConnected := WasConn;
|
||||
Ctrl.FRunning := WasRun;
|
||||
Ctrl.SyncCWKeyer; // разоружить обратно
|
||||
end;
|
||||
Ctrl.SetMode(MODE_USB);
|
||||
|
||||
// Ответ называет приёмник, чей слайс реально в эфире. Без радио источник
|
||||
// передачи — главный VFO, то есть 0.
|
||||
C.SendText('trx:0;');
|
||||
@@ -1938,6 +2115,7 @@ begin
|
||||
TestResampling;
|
||||
TestStreamOut;
|
||||
TestRecorder;
|
||||
TestKeyPlayer;
|
||||
TestServer;
|
||||
TestEndToEnd;
|
||||
WriteLn;
|
||||
|
||||
Reference in New Issue
Block a user