fix(cw): обрыв манипуляции — поколение, а не флаг: пакет следом его отменял

Один дефект в двух местах: и TCWElemPlayer (чужая манипуляция по TCI), и
TCWSender (передача текста — F1..F8, набор в терминале, CAT KY) сбрасывали
FAbortReq прямо в Enqueue. Поток замечает обрыв только на очередном срезе Hold,
то есть в пределах 5 мс; всё, что прилетело в это окно, снимало флаг, и уже
отданный AbortPlay/AbortSending пропадал бесследно.

Чем это кончается в эфире: обрыв существует ровно затем, чтобы касание
манипулятора, снятие MOX или уход из телеграфа прекратили программную
манипуляцию (как «key hit» в Thetis/pihpsdr). Проглоченный обрыв означает, что
оператор взял ключ в руки, а софт продолжает манипулировать вместе с ним.

Источники, кормящие Enqueue, ровно такие, чтобы в 5 мс попадать: клиент TCI
шлёт элементы KEYER десятками в секунду; терминал отдаёт набор В ЭФИР ПО
СИМВОЛУ на нажатие клавиши; поле текста KY — 25 знаков, поэтому длинное
сообщение логгер шлёт несколькими командами подряд. У передачи текста цена
выше: AbortSending чистит FPending, но взятое сообщение живёт в локальной
переменной потока, и доигрывается ВЕСЬ его остаток — замер на стенде дал 620 мс
(остаток 720-мс тире на 5 WPM), у KEYER — 742 мс.

Лечение общее: вместо флага счётчик поколений. AbortPlay/AbortSending его
инкрементируют, Enqueue не трогает вовсе, поток несёт своё поколение (Take,
Aborted, Hold, SendChar) и принимает новое только сам — когда отпустил ключ и
бросил очередь. У TCWSender есть точка получше: текст и поколение берутся одним
заходом под лок (TakeText(out Gen)), а это закрывает заодно и вторую точку
сброса — ту, что стояла в начале Execute. Добор по ходу передачи стал
TakeMore(Gen): после обрыва не берём ничего, и набранное ПОСЛЕ него не теряется
вместе с брошенным, а уходит следующим сообщением со своим поколением.

Стенд: 244 проверки (было 238). В C2 — регрессия на KEYER, новая часть C3 на
передачу текста (раньше TCWSender стендом не покрывался вовсе): длительность
точки по скорости, старт сообщения, обрыв с пакетом следом, и что после обрыва
передача снова идёт. Обе регрессии проверены негативным контролем — на старом
CWMorse.pas они падают с теми самыми 742 и 620 мс.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 10:44:52 +03:00
co-authored by Claude Opus 5
parent 6aa4db3cb8
commit 210f3e6457
2 changed files with 203 additions and 37 deletions
+121
View File
@@ -1039,6 +1039,7 @@ var
Ev: TCWKeyEvent;
i, Waited, Bad: Integer;
D1, D2, D3: Int64;
T0: QWord;
begin
WriteLn('C2. Чужая манипуляция (KEYER)');
Log := TKeyLog.Create;
@@ -1108,6 +1109,32 @@ begin
Check('KEYER: обрыв гасит очередь', Log.Count = Bad, IntToStr(Log.Count - Bad));
Check('KEYER: после обрыва ключ отпущен',
(Log.Count > 0) and (not Log.Down[Log.Count - 1]));
// ★Обрыв нельзя отменить пакетом из сети. Клиент шлёт элементы пачками по
// несколько десятков в секунду; такой пакет, прилетевший в те миллисекунды,
// пока поток спит в Hold, раньше снимал флаг обрыва — оператор трогает
// манипулятор, а чужая манипуляция продолжает держать ключ до конца
// текущего элемента. Меряем задержку отпускания от момента обрыва.
for i := 0 to 4 do P.Enqueue(True, 400);
Sleep(60);
Log.Count := 0; // ключ уже замкнут — ждём именно отпускания
T0 := GetTickCount64;
P.AbortPlay;
P.Enqueue(True, 400); // «пакет пришёл следом за обрывом»
Waited := 0;
while (Log.Count = 0) and (Waited < 1000) do
begin
Sleep(5);
Inc(Waited, 5);
end;
// Отсчёт по стенным часам: Log.At отсчитывается от ПЕРВОГО фронта пачки,
// а нам нужна задержка от самого обрыва (опрос с шагом 5 мс).
D1 := Int64(GetTickCount64) - Int64(T0);
Check('KEYER: обрыв не отменяется пакетом следом',
(Log.Count > 0) and (not Log.Down[0]) and (D1 < 100),
Format('%d/%d', [Log.Count, D1]));
P.AbortPlay; // и хвост этого пакета тоже гасим
Sleep(120);
// Нулевые и отрицательные длительности не элементы: первое нажатие клиента
// (keyer:0,true,0) не должно порождать ни одного фронта.
Log.Count := 0;
@@ -1122,6 +1149,99 @@ begin
end;
end;
procedure TestTextSender;
// Передача текста (F1..F8, набор в терминале, CAT KY). Проверяем не «пошли
// фронты», а что ОБРЫВ работает: касание манипулятора и снятие MOX обязаны
// прекратить программную манипуляцию, чем бы её ни кормили.
var
Log: TKeyLog;
S: TCWSender;
Ev: TCWKeyEvent;
Waited: Integer;
D: Int64;
T0: QWord;
begin
WriteLn('C3. Передача текста (KY / F1..F8)');
Log := TKeyLog.Create;
Ev := Log.Key;
S := TCWSender.Create(Ev);
try
// 'E' на 20 WPM — одна точка длиной в один Dit (1200/20 = 60 мс).
S.SetSpeed(20, 50);
S.Enqueue('E');
Waited := 0;
while (Log.Count < 2) and (Waited < 2000) do
begin
Sleep(5);
Inc(Waited, 5);
end;
Sleep(50);
Check('KY: точка — два фронта', Log.Count = 2, IntToStr(Log.Count));
if Log.Count >= 2 then
begin
D := Int64(Log.At[1]) - Int64(Log.At[0]);
Check('KY: длительность точки по скорости', Abs(D - 60) < 30, IntToStr(D));
end
else
Check('KY: длительность точки по скорости', False);
// ★Обрыв нельзя отменить символом, набранным следом. Терминал шлёт текст
// ПО СИМВОЛУ на нажатие клавиши, логгер — кусками по 25 знаков в KY;
// такой символ, пришедший в те миллисекунды, пока поток спит в Hold,
// раньше снимал флаг обрыва — и в эфир уходил весь остаток сообщения,
// хотя оператор уже взял манипулятор. 5 WPM: тире длиной 720 мс, попасть
// в него легко.
S.SetSpeed(5, 50);
Log.Count := 0;
S.Enqueue('OOOO');
// Ждём не «появился фронт», а замыкания ключа ЭТИМ сообщением: предыдущее
// ещё доигрывает межзнаковую паузу, и его завершающий Key(False) прилетит
// сюда же.
Waited := 0;
while (Waited < 3000)
and not ((Log.Count > 0) and Log.Down[Log.Count - 1]) do
begin
Sleep(5);
Inc(Waited, 5);
end;
Check('KY: сообщение пошло',
(Log.Count > 0) and Log.Down[Log.Count - 1], IntToStr(Log.Count));
Sleep(100); // середина первого тире (720 мс на 5 WPM)
Log.Count := 0; // ключ уже замкнут — ждём именно отпускания
T0 := GetTickCount64;
S.AbortSending;
S.Enqueue('E'); // «символ пришёл следом за обрывом»
Waited := 0;
while (Log.Count = 0) and (Waited < 2000) do
begin
Sleep(5);
Inc(Waited, 5);
end;
D := Int64(GetTickCount64) - Int64(T0);
Check('KY: обрыв не отменяется символом следом',
(Log.Count > 0) and (not Log.Down[0]) and (D < 100),
Format('%d/%d', [Log.Count, D]));
S.AbortSending; // и хвост этого символа тоже гасим
Sleep(150);
// Обрыв не глушит передачу навсегда: следующее сообщение играется.
Log.Count := 0;
S.SetSpeed(20, 50);
S.Enqueue('E');
Waited := 0;
while (Log.Count < 2) and (Waited < 2000) do
begin
Sleep(5);
Inc(Waited, 5);
end;
Check('KY: после обрыва передача снова идёт', Log.Count = 2,
IntToStr(Log.Count));
finally
S.Free;
Log.Free;
end;
end;
{ ═══════════════════════════════════════════════════════════════════════════
D. Живой сервер: команды потоков
═══════════════════════════════════════════════════════════════════════════ }
@@ -2116,6 +2236,7 @@ begin
TestStreamOut;
TestRecorder;
TestKeyPlayer;
TestTextSender;
TestServer;
TestEndToEnd;
WriteLn;