mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-26 04:47:35 +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:
+221
@@ -64,6 +64,68 @@ type
|
||||
function Backspace: Boolean;
|
||||
end;
|
||||
|
||||
{ Элемент манипуляции с ЗАДАННОЙ длительностью: посылка (Mark = True) или
|
||||
пауза. Из таких элементов состоит чужая манипуляция, пришедшая по сети —
|
||||
у неё нет ни скорости, ни веса, есть только измеренные интервалы. }
|
||||
TCWElement = record
|
||||
Mark: Boolean;
|
||||
Ms: Integer;
|
||||
end;
|
||||
|
||||
{ Проигрыватель чужой манипуляции: очередь элементов → тот же колбэк ключа.
|
||||
|
||||
ЗАЧЕМ ОТДЕЛЬНО ОТ TCWSender. Текст мы разбираем сами и знаем скорость; здесь
|
||||
наоборот — знаки не разбираются вовсе, а длительности приходят готовыми
|
||||
(команда TCI KEYER: клиент замеряет свой ключ и присылает длину КАЖДОГО
|
||||
завершившегося интервала). Проиграть их надо ровно так, как они были нажаты.
|
||||
|
||||
★Почему нельзя просто дёргать ключ по приходу пакета. Между клиентом и нами
|
||||
сеть: приход пакета говорит, что интервал КОНЧИЛСЯ, а не сколько он длился.
|
||||
Манипуляция «по приходу» — это сетевой джиттер прямо в эфир, тот самый
|
||||
«пьяный матрос», ради которого в протоколе и заведён третий аргумент.
|
||||
Поэтому элементы становятся в очередь и играются ПОДРЯД по абсолютным
|
||||
дедлайнам: сумма длительностей равна времени у клиента, значит отставание
|
||||
постоянно (сеть + один элемент) и не накапливается.
|
||||
|
||||
Очередь опустела — ключ отпускаем и начинаем следующую пачку с чистого
|
||||
дедлайна: элементы всегда чередуются (посылка, пауза, посылка…), так что
|
||||
отпускание на пустой очереди ничего не искажает, зато не оставляет в эфире
|
||||
несущую, если клиент замолчал или отвалился. }
|
||||
TCWElemPlayer = class(TThread)
|
||||
private
|
||||
FKeyEvent: TCWKeyEvent;
|
||||
FLock: TCriticalSection;
|
||||
FWake: TEvent;
|
||||
FQ: array of TCWElement; // кольцо (под FLock)
|
||||
FHead: Integer;
|
||||
FCount: Integer;
|
||||
FAbortReq: Boolean;
|
||||
FBusy: Boolean;
|
||||
FDown: Boolean; // состояние ключа (только поток)
|
||||
procedure Key(Down: Boolean);
|
||||
function Take(out E: TCWElement): Boolean;
|
||||
function Aborted: Boolean;
|
||||
function Hold(Deadline: QWord): Boolean;
|
||||
protected
|
||||
procedure Execute; override;
|
||||
public
|
||||
constructor Create(AKeyEvent: TCWKeyEvent);
|
||||
destructor Destroy; override;
|
||||
{ Поставить элемент в очередь. Слишком длинные обрезаются вызывающим —
|
||||
здесь принимается то, что дали. }
|
||||
procedure Enqueue(Mark: Boolean; Ms: Integer);
|
||||
{ Бросить очередь и отпустить ключ (оператор тронул манипулятор, снят MOX). }
|
||||
procedure AbortPlay;
|
||||
function Busy: Boolean;
|
||||
function Pending: Integer;
|
||||
end;
|
||||
|
||||
// Сколько элементов держим в очереди. Живая манипуляция даёт ~30 элементов в
|
||||
// секунду, так что это секунды звука: клиент, сыплющий быстрее, чем играется,
|
||||
// нам не друг, и лишнее просто отбрасывается.
|
||||
const
|
||||
CW_ELEM_QUEUE_MAX = 512;
|
||||
|
||||
// Код знака: строка из '.' и '-'. Пустая — знака нет в таблице (пропускаем,
|
||||
// иначе опечатка в макросе превратилась бы в мусор в эфире).
|
||||
function MorseCode(Ch: Char): string;
|
||||
@@ -379,4 +441,163 @@ begin
|
||||
Key(False);
|
||||
end;
|
||||
|
||||
{ ═══════════════════════════════════════════════════════════════════════════
|
||||
Проигрыватель чужой манипуляции
|
||||
═══════════════════════════════════════════════════════════════════════════ }
|
||||
|
||||
constructor TCWElemPlayer.Create(AKeyEvent: TCWKeyEvent);
|
||||
begin
|
||||
FKeyEvent := AKeyEvent;
|
||||
FLock := TCriticalSection.Create;
|
||||
FWake := TEvent.Create(nil, False, False, '');
|
||||
SetLength(FQ, CW_ELEM_QUEUE_MAX);
|
||||
FreeOnTerminate := False;
|
||||
inherited Create(False);
|
||||
end;
|
||||
|
||||
destructor TCWElemPlayer.Destroy;
|
||||
begin
|
||||
Terminate;
|
||||
FWake.SetEvent;
|
||||
WaitFor;
|
||||
FWake.Free;
|
||||
FLock.Free;
|
||||
inherited Destroy;
|
||||
end;
|
||||
|
||||
procedure TCWElemPlayer.Enqueue(Mark: Boolean; Ms: Integer);
|
||||
var i: Integer;
|
||||
begin
|
||||
if Ms <= 0 then Exit;
|
||||
FLock.Enter;
|
||||
try
|
||||
// Очередь переполнена — молча теряем новый элемент. Ронять уже принятую
|
||||
// манипуляцию из-за захлебнувшегося клиента незачем.
|
||||
if FCount >= Length(FQ) then Exit;
|
||||
i := (FHead + FCount) mod Length(FQ);
|
||||
FQ[i].Mark := Mark;
|
||||
FQ[i].Ms := Ms;
|
||||
Inc(FCount);
|
||||
FAbortReq := False;
|
||||
FBusy := True;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
FWake.SetEvent;
|
||||
end;
|
||||
|
||||
function TCWElemPlayer.Take(out E: TCWElement): Boolean;
|
||||
begin
|
||||
Result := False;
|
||||
FLock.Enter;
|
||||
try
|
||||
if FAbortReq or (FCount = 0) then Exit;
|
||||
E := FQ[FHead];
|
||||
FHead := (FHead + 1) mod Length(FQ);
|
||||
Dec(FCount);
|
||||
Result := True;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TCWElemPlayer.AbortPlay;
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
FAbortReq := True;
|
||||
FHead := 0;
|
||||
FCount := 0;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
FWake.SetEvent;
|
||||
end;
|
||||
|
||||
function TCWElemPlayer.Aborted: Boolean;
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
Result := FAbortReq;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TCWElemPlayer.Busy: Boolean;
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
Result := FBusy;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TCWElemPlayer.Pending: Integer;
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
Result := FCount;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TCWElemPlayer.Key(Down: Boolean);
|
||||
begin
|
||||
if Down = FDown then Exit; // лишних фронтов в эфир не шлём
|
||||
FDown := Down;
|
||||
if Assigned(FKeyEvent) then FKeyEvent(Down);
|
||||
end;
|
||||
|
||||
function TCWElemPlayer.Hold(Deadline: QWord): Boolean;
|
||||
// Тот же приём, что у TCWSender: абсолютный дедлайн и дробный сон, чтобы обрыв
|
||||
// не ждал конца длинного тире.
|
||||
var Now_: QWord; Slice: Integer;
|
||||
begin
|
||||
repeat
|
||||
if Terminated or Aborted then Exit(False);
|
||||
Now_ := GetTickCount64;
|
||||
if Now_ >= Deadline then Exit(True);
|
||||
Slice := Integer(Deadline - Now_);
|
||||
if Slice > 5 then Slice := 5;
|
||||
Sleep(Slice);
|
||||
until False;
|
||||
end;
|
||||
|
||||
procedure TCWElemPlayer.Execute;
|
||||
var
|
||||
E: TCWElement;
|
||||
Deadline: QWord;
|
||||
begin
|
||||
Deadline := 0;
|
||||
while not Terminated do
|
||||
begin
|
||||
FWake.WaitFor(200);
|
||||
if Terminated then Break;
|
||||
while Take(E) do
|
||||
begin
|
||||
// Первая пачка (или после простоя) начинается «сейчас»: догонять прошлое
|
||||
// нечего, а старый дедлайн проиграл бы очередь одним махом.
|
||||
if (Deadline = 0) or (GetTickCount64 > Deadline) then
|
||||
Deadline := GetTickCount64;
|
||||
Key(E.Mark);
|
||||
Inc(Deadline, QWord(E.Ms));
|
||||
if not Hold(Deadline) then Break;
|
||||
end;
|
||||
// Очередь кончилась (или её бросили) — ключ отпускаем всегда: несущая без
|
||||
// хозяина в эфире не остаётся, а следующая пачка начнётся заново.
|
||||
Key(False);
|
||||
Deadline := 0;
|
||||
FLock.Enter;
|
||||
try
|
||||
if FCount = 0 then FBusy := False;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
Key(False);
|
||||
end;
|
||||
|
||||
end.
|
||||
|
||||
Reference in New Issue
Block a user