mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +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;
|
function Backspace: Boolean;
|
||||||
end;
|
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;
|
function MorseCode(Ch: Char): string;
|
||||||
@@ -379,4 +441,163 @@ begin
|
|||||||
Key(False);
|
Key(False);
|
||||||
end;
|
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.
|
end.
|
||||||
|
|||||||
+55
-2
@@ -507,6 +507,9 @@ type
|
|||||||
// Программная передача текста (бит CWX). Поток создаётся лениво — у тех,
|
// Программная передача текста (бит CWX). Поток создаётся лениво — у тех,
|
||||||
// кто телеграфом не пользуется, лишней нити не заводится.
|
// кто телеграфом не пользуется, лишней нити не заводится.
|
||||||
FCWSender: TCWSender;
|
FCWSender: TCWSender;
|
||||||
|
// Чужая манипуляция по сети (TCI KEYER): очередь готовых интервалов.
|
||||||
|
// Тоже лениво — заводится на первом элементе.
|
||||||
|
FCWElem: TCWElemPlayer;
|
||||||
// Состояние ключа для ПРОГРАММНОГО сайдтона. Пишется потоком передачи и
|
// Состояние ключа для ПРОГРАММНОГО сайдтона. Пишется потоком передачи и
|
||||||
// разбором HP-статуса (прямой ключ), читается DSP-потоком в MixSidetone —
|
// разбором HP-статуса (прямой ключ), читается DSP-потоком в MixSidetone —
|
||||||
// толерантная гонка на одном булеве, как FHPp*.
|
// толерантная гонка на одном булеве, как FHPp*.
|
||||||
@@ -723,6 +726,7 @@ type
|
|||||||
function CWOptionsByte: Byte; // DUC Specific байт 5
|
function CWOptionsByte: Byte; // DUC Specific байт 5
|
||||||
procedure SyncCWKeyer; // байт 5 под текущий TX-режим
|
procedure SyncCWKeyer; // байт 5 под текущий TX-режим
|
||||||
procedure CWXKeyEvent(Down: Boolean); // колбэк потока передачи
|
procedure CWXKeyEvent(Down: Boolean); // колбэк потока передачи
|
||||||
|
procedure CWElemKey(Down: Boolean); // колбэк проигрывателя KEYER
|
||||||
// Сдвиг несущей манипуляции от гетеродина (уход от утечки LO у zero-IF).
|
// Сдвиг несущей манипуляции от гетеродина (уход от утечки LO у zero-IF).
|
||||||
// На него же уезжает DUC/LO, чтобы РЧ встала ровно на VFO — см. TXTuneFreqHz.
|
// На него же уезжает DUC/LO, чтобы РЧ встала ровно на VFO — см. TXTuneFreqHz.
|
||||||
function CWCarrierOffsetHz: Double;
|
function CWCarrierOffsetHz: Double;
|
||||||
@@ -1128,6 +1132,10 @@ type
|
|||||||
// Thetis cwx.cs). Занят ли передатчик — CWXBusy; ЛЮБОЕ касание
|
// Thetis cwx.cs). Занят ли передатчик — CWXBusy; ЛЮБОЕ касание
|
||||||
// манипулятора или снятие MOX обрывает передачу (CWXAbort).
|
// манипулятора или снятие MOX обрывает передачу (CWXAbort).
|
||||||
procedure CWXSend(const Text: string);
|
procedure CWXSend(const Text: string);
|
||||||
|
// Чужая манипуляция: один готовый интервал (посылка или пауза) заданной
|
||||||
|
// длины. Так приходит телеграф по TCI (команда KEYER) — знаков там нет,
|
||||||
|
// есть измеренные клиентом длительности. См. TCWElemPlayer.
|
||||||
|
procedure CWKeyerElement(Mark: Boolean; Ms: Integer);
|
||||||
procedure CWXAbort;
|
procedure CWXAbort;
|
||||||
function CWXBusy: Boolean;
|
function CWXBusy: Boolean;
|
||||||
// ---- Окно-терминал -----------------------------------------------------
|
// ---- Окно-терминал -----------------------------------------------------
|
||||||
@@ -1534,6 +1542,11 @@ begin
|
|||||||
FCWSender.AbortSending;
|
FCWSender.AbortSending;
|
||||||
FreeAndNil(FCWSender);
|
FreeAndNil(FCWSender);
|
||||||
end;
|
end;
|
||||||
|
if Assigned(FCWElem) then
|
||||||
|
begin
|
||||||
|
FCWElem.AbortPlay;
|
||||||
|
FreeAndNil(FCWElem);
|
||||||
|
end;
|
||||||
if Assigned(FCWDec) then FCWDec.Enabled := False; // кольцо больше не кормим
|
if Assigned(FCWDec) then FCWDec.Enabled := False; // кольцо больше не кормим
|
||||||
FreeEngines;
|
FreeEngines;
|
||||||
// Декодер — ПОСЛЕ движка: его кольцо наполняет DSP-поток, и освобождать
|
// Декодер — ПОСЛЕ движка: его кольцо наполняет DSP-поток, и освобождать
|
||||||
@@ -6584,7 +6597,8 @@ begin
|
|||||||
// на бэнд с DoNotTx, правка Alex. Перечислять такие точки поимённо — гарантия
|
// на бэнд с DoNotTx, правка Alex. Перечислять такие точки поимённо — гарантия
|
||||||
// однажды пропустить очередную (ровно так уже вышло с DUC-частотой и мощностью).
|
// однажды пропустить очередную (ровно так уже вышло с DUC-частотой и мощностью).
|
||||||
if (not (ActiveTXMode in [MODE_CWL, MODE_CWU]))
|
if (not (ActiveTXMode in [MODE_CWL, MODE_CWU]))
|
||||||
and Assigned(FCWSender) and FCWSender.Busy then CWXAbort;
|
and ((Assigned(FCWSender) and FCWSender.Busy)
|
||||||
|
or (Assigned(FCWElem) and FCWElem.Busy)) then CWXAbort;
|
||||||
// Вооружённый кейер = железо может выйти в эфир само ⇒ бэкенд обязан класть
|
// Вооружённый кейер = железо может выйти в эфир само ⇒ бэкенд обязан класть
|
||||||
// уровень мощности в КАЖДЫЙ HP-кадр, а не только на передаче. Локальный
|
// уровень мощности в КАЖДЫЙ HP-кадр, а не только на передаче. Локальный
|
||||||
// генератор — та же история: FTransmitting в телеграфе не поднимается, а
|
// генератор — та же история: FTransmitting в телеграфе не поднимается, а
|
||||||
@@ -6759,7 +6773,43 @@ end;
|
|||||||
function TRadioController.CWXBusy: Boolean;
|
function TRadioController.CWXBusy: Boolean;
|
||||||
begin
|
begin
|
||||||
Result := (Assigned(FCWSender) and FCWSender.Busy)
|
Result := (Assigned(FCWSender) and FCWSender.Busy)
|
||||||
or (Assigned(FCWLocal) and FCWLocal.Busy);
|
or (Assigned(FCWLocal) and FCWLocal.Busy)
|
||||||
|
or (Assigned(FCWElem) and FCWElem.Busy);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.CWElemKey(Down: Boolean);
|
||||||
|
// Поток проигрывателя. Чужая манипуляция — это ПРЯМОЙ КЛЮЧ, только с точными
|
||||||
|
// длительностями, поэтому и уходит она в те же две двери, что и ключ оператора:
|
||||||
|
// у Pluto — во вход прямого ключа локального генератора (он сам поднимает
|
||||||
|
// сессию, рисует огибающую и сайдтон), у openHPSDR — в бит CWX прошивки (тем же
|
||||||
|
// путём идёт и передача текста).
|
||||||
|
begin
|
||||||
|
if CWLocalSource then
|
||||||
|
begin
|
||||||
|
if Assigned(FCWLocal) then FCWLocal.StraightKey(Down);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
CWXKeyEvent(Down);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.CWKeyerElement(Mark: Boolean; Ms: Integer);
|
||||||
|
// Поток контроллера (адаптер TCI маршалит через Invoke). Ставим элемент в
|
||||||
|
// очередь — играет её отдельный поток по абсолютным дедлайнам.
|
||||||
|
begin
|
||||||
|
if Ms <= 0 then Exit;
|
||||||
|
// Тот же гейт, что у CWXSend: CWTXActive учитывает и режим, и запрет
|
||||||
|
// передачи (RX-only слот трансвертера, бэнд с DoNotTx) — там кейер разоружён,
|
||||||
|
// и трогать ключ незачем.
|
||||||
|
if not CWTXActive then Exit;
|
||||||
|
// У Pluto манипуляцию рисуем сами: генератор обязан быть заведён и вооружён,
|
||||||
|
// иначе первый же элемент уйдёт в никуда (то же делает CWXSend).
|
||||||
|
if CWLocalSource then
|
||||||
|
begin
|
||||||
|
EnsureCWLocal;
|
||||||
|
SyncCWKeyer;
|
||||||
|
end;
|
||||||
|
if not Assigned(FCWElem) then FCWElem := TCWElemPlayer.Create(Self.CWElemKey);
|
||||||
|
FCWElem.Enqueue(Mark, Ms);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.CWXSend(const Text: string);
|
procedure TRadioController.CWXSend(const Text: string);
|
||||||
@@ -6790,6 +6840,9 @@ procedure TRadioController.CWXAbort;
|
|||||||
begin
|
begin
|
||||||
if Assigned(FCWSender) then FCWSender.AbortSending;
|
if Assigned(FCWSender) then FCWSender.AbortSending;
|
||||||
if Assigned(FCWLocal) then FCWLocal.AbortText;
|
if Assigned(FCWLocal) then FCWLocal.AbortText;
|
||||||
|
// Чужая манипуляция обрывается тем же движением: касание манипулятора,
|
||||||
|
// снятие MOX и уход из телеграфа гасят её так же, как передачу текста.
|
||||||
|
if Assigned(FCWElem) then FCWElem.AbortPlay;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TRadioController.CWXBackspace: Boolean;
|
function TRadioController.CWXBackspace: Boolean;
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ type
|
|||||||
procedure SyncSetCWDelay;
|
procedure SyncSetCWDelay;
|
||||||
procedure SyncCWSend;
|
procedure SyncCWSend;
|
||||||
procedure SyncCWStop;
|
procedure SyncCWStop;
|
||||||
|
procedure SyncKeyerElement;
|
||||||
procedure SyncFocus;
|
procedure SyncFocus;
|
||||||
procedure SyncTaps; // навесить/снять тапы аудио и IQ
|
procedure SyncTaps; // навесить/снять тапы аудио и IQ
|
||||||
|
|
||||||
@@ -334,6 +335,7 @@ type
|
|||||||
// ── Отдельные команды (чтобы HandleCommand не превратился в простыню) ──
|
// ── Отдельные команды (чтобы HandleCommand не превратился в простыню) ──
|
||||||
procedure CmdFreq(Client: TTCIClient; const M: TTCIMessage; IsIF: Boolean);
|
procedure CmdFreq(Client: TTCIClient; const M: TTCIMessage; IsIF: Boolean);
|
||||||
procedure CmdCWMacros(const M: TTCIMessage; IsMsg: Boolean);
|
procedure CmdCWMacros(const M: TTCIMessage; IsMsg: Boolean);
|
||||||
|
procedure CmdKeyer(Client: TTCIClient; const M: TTCIMessage);
|
||||||
procedure CmdSpot(const M: TTCIMessage);
|
procedure CmdSpot(const M: TTCIMessage);
|
||||||
public
|
public
|
||||||
constructor Create(AController: TRadioController; ASpots: TDXSpotStore = nil);
|
constructor Create(AController: TRadioController; ASpots: TDXSpotStore = nil);
|
||||||
@@ -1673,6 +1675,12 @@ begin
|
|||||||
FController.CWXAbort;
|
FController.CWXAbort;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TTCIAdapter.SyncKeyerElement;
|
||||||
|
// Поток контроллера. FsBool — посылка (иначе пауза), FsInt — её длительность.
|
||||||
|
begin
|
||||||
|
FController.CWKeyerElement(FsBool, FsInt);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TTCIAdapter.SyncFocus;
|
procedure TTCIAdapter.SyncFocus;
|
||||||
// SET_IN_FOCUS: поднять окно программы. Само окно адаптеру недоступно (он
|
// SET_IN_FOCUS: поднять окно программы. Само окно адаптеру недоступно (он
|
||||||
// равноправный клиент контроллера) — действие ставит UI через OnFocusRequest.
|
// равноправный клиент контроллера) — действие ставит UI через OnFocusRequest.
|
||||||
@@ -1785,6 +1793,64 @@ begin
|
|||||||
FServer.Broadcast(TCIBuild('callsign_send', [Call]));
|
FServer.Broadcast(TCIBuild('callsign_send', [Call]));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TTCIAdapter.CmdKeyer(Client: TTCIClient; const M: TTCIMessage);
|
||||||
|
// KEYER:arg1,arg2,arg3; — arg1 передатчик, arg2 «ключ нажат», arg3 длительность
|
||||||
|
// ПРЕДЫДУЩЕГО интервала в мс (§4.3, «однонаправленное управление»).
|
||||||
|
//
|
||||||
|
// ★Ключ к команде — что arg3 описывает интервал, который ТОЛЬКО ЧТО кончился, а
|
||||||
|
// не тот, который начинается. Документ описывает это алгоритмом: первое нажатие
|
||||||
|
// даёт keyer:0,true,0, отпускание — keyer:0,false,142, то есть «посылка длилась
|
||||||
|
// 142 мс», следующее нажатие — keyer:0,true,58, то есть «пауза длилась 58 мс».
|
||||||
|
// Отсюда правило перевода в элементы: тип интервала — это состояние ключа ДО
|
||||||
|
// фронта, то есть ОБРАТНОЕ пришедшему (пришло false — кончилась посылка).
|
||||||
|
// Первое сообщение (arg3 = 0) играть нечего: оно лишь открывает передачу.
|
||||||
|
//
|
||||||
|
// Почему элементы, а не «дёрнуть ключ прямо сейчас»: между клиентом и нами
|
||||||
|
// сеть, и манипуляция по приходу пакетов — это её джиттер в эфире. Ради этого
|
||||||
|
// в протоколе и заведён третий аргумент; проигрывает очередь TCWElemPlayer.
|
||||||
|
var
|
||||||
|
Rx, Ms: Integer;
|
||||||
|
Down: Boolean;
|
||||||
|
begin
|
||||||
|
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
|
||||||
|
begin
|
||||||
|
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'bad receiver']));
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
// Номер объявлен потолком железа, но пана под ним может не быть — как у TRX.
|
||||||
|
if (Rx > 0) and not RxActive(Rx) then
|
||||||
|
begin
|
||||||
|
Reply(Client, TCIBuild('tci_error',
|
||||||
|
[LowerCase(M.Name), 'receiver is not running']));
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
if not TCITryArgBool(M, 1, Down) then
|
||||||
|
begin
|
||||||
|
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'bad state']));
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
if not TCITryArgInt(M, 2, Ms) then Ms := 0;
|
||||||
|
if Ms <= 0 then Exit; // первое нажатие: играть ещё нечего
|
||||||
|
// Ключ — это передатчик, а он один: держит его тот же, кто держит TRX (§3.5).
|
||||||
|
// Чужую передачу с чужого ключа не портим.
|
||||||
|
if not Claim(HoldKey('TRX', 0, 0), Client) then Exit;
|
||||||
|
if Down then
|
||||||
|
begin
|
||||||
|
// Кончилась ПАУЗА (ключ был отпущен).
|
||||||
|
if Ms > TCI_KEYER_GAP_MAX_MS then Ms := TCI_KEYER_GAP_MAX_MS;
|
||||||
|
end
|
||||||
|
else if Ms > TCI_KEYER_MARK_MAX_MS then
|
||||||
|
Ms := TCI_KEYER_MARK_MAX_MS;
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FsBool := not Down; // посылка, если ключ ОТПУСТИЛИ
|
||||||
|
FsInt := Ms;
|
||||||
|
if CanInvoke then FController.Invoke(SyncKeyerElement);
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TTCIAdapter.CmdSpot(const M: TTCIMessage);
|
procedure TTCIAdapter.CmdSpot(const M: TTCIMessage);
|
||||||
// SPOT:позывной,мода,частота,цвет ARGB,текст;
|
// SPOT:позывной,мода,частота,цвет ARGB,текст;
|
||||||
var
|
var
|
||||||
@@ -2472,6 +2538,7 @@ begin
|
|||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
if M.Name = 'KEYER' then begin CmdKeyer(Client, M); Exit; end;
|
||||||
if M.Name = 'CW_MACROS' then begin CmdCWMacros(M, False); Exit; end;
|
if M.Name = 'CW_MACROS' then begin CmdCWMacros(M, False); Exit; end;
|
||||||
if M.Name = 'CW_MSG' then begin CmdCWMacros(M, True); Exit; end;
|
if M.Name = 'CW_MSG' then begin CmdCWMacros(M, True); Exit; end;
|
||||||
if M.Name = 'CW_MACROS_STOP' then
|
if M.Name = 'CW_MACROS_STOP' then
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ const
|
|||||||
TCI_TX_BUFFERING_MIN = 50;
|
TCI_TX_BUFFERING_MIN = 50;
|
||||||
TCI_TX_BUFFERING_MAX = 500;
|
TCI_TX_BUFFERING_MAX = 500;
|
||||||
TCI_RECORD_MAX_SEC = 300; // потолок записи линейного выхода
|
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 МБ,
|
// 1 + MAX_SLICES, и предельные 300 с на каждом — это 57.6 МБ × 7 ≈ 403 МБ,
|
||||||
// которые неавторизованный клиент выпрашивал бы семью строками. Бюджет
|
// которые неавторизованный клиент выпрашивал бы семью строками. Бюджет
|
||||||
|
|||||||
+69
-23
@@ -331,8 +331,9 @@ B независимо от того, где он живёт: на openHPSDR э
|
|||||||
`SPOT`, `SPOT_DELETE`, `SPOT_CLEAR` (в `TDXSpotStore`, спот виден на всех
|
`SPOT`, `SPOT_DELETE`, `SPOT_CLEAR` (в `TDXSpotStore`, спот виден на всех
|
||||||
панадаптерах; время спота — UTC, как у кластера, а не местное),
|
панадаптерах; время спота — UTC, как у кластера, а не местное),
|
||||||
`RX_SENSORS_ENABLE`, `TX_SENSORS_ENABLE` (период — на клиента),
|
`RX_SENSORS_ENABLE`, `TX_SENSORS_ENABLE` (период — на клиента),
|
||||||
|
`KEYER` (чужой ключ — см. §2.6),
|
||||||
`IQ_SAMPLERATE`, `AUDIO_SAMPLERATE`, `AUDIO_STREAM_*`, `TX_STREAM_AUDIO_BUFFERING`
|
`IQ_SAMPLERATE`, `AUDIO_SAMPLERATE`, `AUDIO_STREAM_*`, `TX_STREAM_AUDIO_BUFFERING`
|
||||||
(значения принимаются и подтверждаются; сами потоки — этап 2). Параметры
|
(значения принимаются и подтверждаются). Параметры
|
||||||
потоков — настройки **клиента**, а не устройства: живут в `TTCIClient`, и один
|
потоков — настройки **клиента**, а не устройства: живут в `TTCIClient`, и один
|
||||||
клиент не переопределяет их остальным; подписки и параметры читаются/пишутся
|
клиент не переопределяет их остальным; подписки и параметры читаются/пишутся
|
||||||
под локом клиента, потому что пишет их его поток, а читает тик-поток.
|
под локом клиента, потому что пишет их его поток, а читает тик-поток.
|
||||||
@@ -343,8 +344,7 @@ int16/int24/int32/float32. Чужое значение не принимаетс
|
|||||||
действующее.
|
действующее.
|
||||||
|
|
||||||
Команды **запуска** потоков (`IQ_START`/`IQ_STOP`, `AUDIO_START`/`AUDIO_STOP`,
|
Команды **запуска** потоков (`IQ_START`/`IQ_STOP`, `AUDIO_START`/`AUDIO_STOP`,
|
||||||
`LINE_OUT_*`) отвечают `tci_error:<команда>,binary streams are not implemented`.
|
`LINE_OUT_*`) разобраны в §2.5 — этап 2 сделан, и потоки настоящие.
|
||||||
Молчать нельзя: клиент решил бы, что поток пошёл, и ждал бы данных бесконечно.
|
|
||||||
|
|
||||||
### 2.4 Уведомления (§4.4, §4.5)
|
### 2.4 Уведомления (§4.4, §4.5)
|
||||||
|
|
||||||
@@ -699,7 +699,7 @@ ExpertSDR3 давно бы не было.
|
|||||||
|
|
||||||
### 2.6 Телеграф (§3.2)
|
### 2.6 Телеграф (§3.2)
|
||||||
|
|
||||||
`CW_MACROS`, `CW_MSG`, `CW_MACROS_STOP`, `CW_TERMINAL`.
|
`CW_MACROS`, `CW_MSG`, `CW_MACROS_STOP`, `CW_TERMINAL`, `KEYER`.
|
||||||
|
|
||||||
Текст приводится к тому, что понимает передатчик текста ewsdr (`CWXSend`):
|
Текст приводится к тому, что понимает передатчик текста ewsdr (`CWXSend`):
|
||||||
экранирование `^ ~ *` снимается, `CALL$N` разворачивается в N повторов
|
экранирование `^ ~ *` снимается, `CALL$N` разворачивается в N повторов
|
||||||
@@ -709,6 +709,52 @@ ExpertSDR3 давно бы не было.
|
|||||||
скорости и не знает прос-знаков. Доотправка позывного (`cw_msg:arg1;`)
|
скорости и не знает прос-знаков. Доотправка позывного (`cw_msg:arg1;`)
|
||||||
игнорируется: уже отданный в очередь текст не редактируется.
|
игнорируется: уже отданный в очередь текст не редактируется.
|
||||||
|
|
||||||
|
**`KEYER` — чужой ключ (§4.3).** `KEYER:<передатчик>,<нажата>,<мс>` — не «нажми
|
||||||
|
сейчас», а **описание уже закончившегося интервала**. Документ задаёт это
|
||||||
|
алгоритмом: первое нажатие даёт `keyer:0,true,0`, отпускание —
|
||||||
|
`keyer:0,false,142` («посылка длилась 142 мс»), следующее нажатие —
|
||||||
|
`keyer:0,true,58` («пауза длилась 58 мс»). Отсюда правило перевода: тип
|
||||||
|
интервала — это состояние ключа **до** фронта, то есть обратное пришедшему;
|
||||||
|
`arg3 = 0` играть нечего, это лишь открытие передачи.
|
||||||
|
|
||||||
|
★Ключевое место — **почему нельзя дёргать ключ по приходу пакета**. Приход
|
||||||
|
говорит, что интервал кончился, а не сколько он длился: манипуляция «по
|
||||||
|
приходу» — это сетевой джиттер прямо в эфир, тот самый «пьяный матрос», ради
|
||||||
|
которого третий аргумент в протоколе и появился. Поэтому элементы становятся в
|
||||||
|
очередь и играются подряд по абсолютным дедлайнам (`TCWElemPlayer` в
|
||||||
|
`CWMorse.pas`): сумма длительностей равна времени у клиента, значит отставание
|
||||||
|
постоянно (сеть + один элемент) и не накапливается.
|
||||||
|
|
||||||
|
Дальше элементы уходят туда же, куда ключ оператора, — чужая манипуляция это и
|
||||||
|
есть прямой ключ, только с точными длительностями: у Pluto во вход прямого
|
||||||
|
ключа локального генератора (он сам поднимает сессию, рисует огибающую и
|
||||||
|
сайдтон), у openHPSDR в бит `CWX` прошивки (тем же путём идёт передача текста).
|
||||||
|
Гейт тот же, что у `CWXSend`, — `CWTXActive`: не телеграфный режим, RX-only
|
||||||
|
слот трансвертера или бэнд с `DoNotTx` — кейер разоружён, и трогать ключ
|
||||||
|
незачем. Передачу `KEYER` не поднимает: как и у текста, при включённом break-in
|
||||||
|
PTT даёт сама прошивка (или сессия локального генератора), при выключенном
|
||||||
|
оператор держит MOX сам.
|
||||||
|
|
||||||
|
Три оговорки, о которых честно:
|
||||||
|
|
||||||
|
- **очередь опустела — ключ отпускается**, и следующая пачка начинается с
|
||||||
|
чистого дедлайна. Элементы всегда чередуются, так что искажения тут нет,
|
||||||
|
зато оборвавшийся клиент не оставляет в эфире несущую;
|
||||||
|
- **паузы обрезаются** `TCI_KEYER_GAP_MAX_MS` = 1 с, посылки —
|
||||||
|
`TCI_KEYER_MARK_MAX_MS` = 5 с. Пауза целиком прибавляется к отставанию от
|
||||||
|
клиента; всё, что дольше секунды, — это «оператор задумался», и честнее
|
||||||
|
догнать реальное время, чем тащить его дальше. Посылка длиннее пяти секунд —
|
||||||
|
уже не телеграф, а залипший ключ;
|
||||||
|
- **обрыв — общий с текстом**: касание манипулятора, снятие MOX, уход из
|
||||||
|
телеграфа гасят чужую манипуляцию тем же `CWXAbort`. Оператор всегда сильнее
|
||||||
|
сети.
|
||||||
|
|
||||||
|
`arg1` проверяется, как у `TRX` (не число или вне `TRX_COUNT` —
|
||||||
|
`bad receiver`; номер в потолке, но пана под ним нет — `receiver is not
|
||||||
|
running`), а сам телеграф уходит **текущему TX-источнику** — ровно так же, как
|
||||||
|
`CW_MACROS`. Захват (§3.5) общий с `TRX`: передатчик один, и ключ держит тот
|
||||||
|
же, кто держит эфир.
|
||||||
|
|
||||||
### 2.7 Кто именно уходит в эфир (`TRX`, `TUNE`)
|
### 2.7 Кто именно уходит в эфир (`TRX`, `TUNE`)
|
||||||
|
|
||||||
★`arg1` у этих двух команд — **номер передатчика**, и он не декорация. Клиент
|
★`arg1` у этих двух команд — **номер передатчика**, и он не декорация. Клиент
|
||||||
@@ -832,7 +878,7 @@ ExpertSDR3 давно бы не было.
|
|||||||
| `DDS` у приёмника-слайса | только читается (центр его панорамы): панорама под слайсом общая, и увести её по просьбе одного клиента значит утащить соседей по пану и картинку оператора. Слайсу двигаться незачем — за окном DDC следит `TuneSliceInBand` |
|
| `DDS` у приёмника-слайса | только читается (центр его панорамы): панорама под слайсом общая, и увести её по просьбе одного клиента значит утащить соседей по пану и картинку оператора. Слайсу двигаться незачем — за окном DDC следит `TuneSliceInBand` |
|
||||||
| Панорама без слайсов | в TCI не видна вовсе: приёмник = слайс. Слушать там нечего, но и IQ такой панорамы клиенту недоступен |
|
| Панорама без слайсов | в TCI не видна вовсе: приёмник = слайс. Слушать там нечего, но и IQ такой панорамы клиенту недоступен |
|
||||||
| Цвет спота (`SPOT`, arg4 ARGB) | не читается: `TDXSpot` цвета не хранит, подписи красятся по моде/возрасту |
|
| Цвет спота (`SPOT`, arg4 ARGB) | не читается: `TDXSpot` цвета не хранит, подписи красятся по моде/возрасту |
|
||||||
| `KEYER`, `TX_FOOTSWITCH` | не реализованы, причины разные — см. ниже под таблицей |
|
| `TX_FOOTSWITCH` | не реализована — см. ниже под таблицей (`KEYER` сделан, см. §2.6) |
|
||||||
| Канал B | есть только у приёмника 0 (VFO B). У приёмника-слайса канал один: слайс — это и есть «приёмник» целиком, со своими модой, фильтром, АРУ, шумодавами и потоками |
|
| Канал B | есть только у приёмника 0 (VFO B). У приёмника-слайса канал один: слайс — это и есть «приёмник» целиком, со своими модой, фильтром, АРУ, шумодавами и потоками |
|
||||||
| Захват параметра (§3.5) | реализован для того, что клиенты действительно перетягивают (частота, DDS, мода, фильтр, TRX/TUNE/DRIVE, split, громкости, АРУ, шумодавы, squelch, скорость CW). Эхо-параметры (RIT/XIT, BIN/ANC/…) не захватываются: на радио они не влияют |
|
| Захват параметра (§3.5) | реализован для того, что клиенты действительно перетягивают (частота, DDS, мода, фильтр, TRX/TUNE/DRIVE, split, громкости, АРУ, шумодавы, squelch, скорость CW). Эхо-параметры (RIT/XIT, BIN/ANC/…) не захватываются: на радио они не влияют |
|
||||||
| Браузерные клиенты | отвергаются по `Origin` (403), см. §1.1. Web-интерфейсу ewsdr TCI не нужен — у него свой канал |
|
| Браузерные клиенты | отвергаются по `Origin` (403), см. §1.1. Web-интерфейсу ewsdr TCI не нужен — у него свой канал |
|
||||||
@@ -845,18 +891,8 @@ ExpertSDR3 давно бы не было.
|
|||||||
| `IQ_SAMPLERATE` 384 кГц на Pluto 576/960 кГц | нацело не делится, поэтому уходит 192 кГц (см. §2.5). Клиент обязан читать частоту из заголовка блока, а не считать её равной запрошенной |
|
| `IQ_SAMPLERATE` 384 кГц на Pluto 576/960 кГц | нацело не делится, поэтому уходит 192 кГц (см. §2.5). Клиент обязан читать частоту из заголовка блока, а не считать её равной запрошенной |
|
||||||
| MP3 у рекордера | не поддержан: кодера в проекте нет, а тащить внешний (lame) ради рекордера — это новая зависимость и её лицензия в сборке, которых у ewsdr сейчас нигде нет. WAV пишется без потерь и открывается всем; на `.mp3` уходит честный `tci_error`, а не молчаливый WAV с чужим расширением |
|
| MP3 у рекордера | не поддержан: кодера в проекте нет, а тащить внешний (lame) ради рекордера — это новая зависимость и её лицензия в сборке, которых у ewsdr сейчас нигде нет. WAV пишется без потерь и открывается всем; на `.mp3` уходит честный `tci_error`, а не молчаливый WAV с чужим расширением |
|
||||||
|
|
||||||
**`KEYER` и `TX_FOOTSWITCH` — почему их нет.** Команды противоположные по
|
**`TX_FOOTSWITCH` — почему его нет.** (`KEYER` был в этом же пункте и теперь
|
||||||
направлению, и мешать их в один пункт «не сделано» неправильно.
|
сделан — см. §2.6: очередь элементов по `arg3`, а не проброс сетевых фронтов.)
|
||||||
|
|
||||||
`KEYER` шлёт **клиент**: это его «клоподав», прокинутый к нам, и третий
|
|
||||||
аргумент — длительность предыдущего знака — существует ровно затем, чтобы
|
|
||||||
телеграфное ядро воспроизвело тайминг точно. У нас несущую в CW даёт либо
|
|
||||||
прошивка (бит CWX / аппаратный кейер), либо программный кейер для Pluto, и оба
|
|
||||||
берут фронты **как пришли**. Прокинуть в них фронты из WebSocket значит
|
|
||||||
отправить в эфир сетевой джиттер, а `arg3` подставить некуда: команды «сыграй
|
|
||||||
знак длиной 142 мс» у openHPSDR нет. Точка входа готова (`CWXKeyEvent`), но
|
|
||||||
делать это надо вместе с очередью знаков по `arg3` — и после того, как весь
|
|
||||||
передающий тракт TCI пройдёт живого клиента (он ещё не проверялся).
|
|
||||||
|
|
||||||
`TX_FOOTSWITCH` шлёт **сервер**, и это просто не выведено: флаг у контроллера
|
`TX_FOOTSWITCH` шлёт **сервер**, и это просто не выведено: флаг у контроллера
|
||||||
уже есть — `HPS_PTT` из HP-статуса ловится по фронту (`FHPpHWPTT` →
|
уже есть — `HPS_PTT` из HP-статуса ловится по фронту (`FHPpHWPTT` →
|
||||||
@@ -892,15 +928,16 @@ ExpertSDR3 давно бы не было.
|
|||||||
рабочий слайс вместе с его модой, фильтром и портом. Правильный порядок:
|
рабочий слайс вместе с его модой, фильтром и портом. Правильный порядок:
|
||||||
сперва завести создание слайса в контроллер (от этого выиграет и демон), и
|
сперва завести создание слайса в контроллер (от этого выиграет и демон), и
|
||||||
только потом привязать к нему команду.
|
только потом привязать к нему команду.
|
||||||
2. **`KEYER`** — см. разбор под таблицей §3.1: нужна очередь знаков по `arg3`,
|
2. **TCI в демоне.** Юниты LCL-free (стенд собирает и гоняет их вместе с
|
||||||
а не проброс сетевых фронтов в ключ.
|
|
||||||
3. **TCI в демоне.** Юниты LCL-free (стенд собирает и гоняет их вместе с
|
|
||||||
`TRadioController` без единого виджета), подключается одной строкой в
|
`TRadioController` без единого виджета), подключается одной строкой в
|
||||||
`ewsdrd.lpr`, как web. Не сделано намеренно и в этом порядке: в headless
|
`ewsdrd.lpr`, как web. Не сделано намеренно и в этом порядке: в headless
|
||||||
сперва попадает то, что уже прошло живого клиента в GUI, где отказ виден
|
сперва попадает то, что уже прошло живого клиента в GUI, где отказ виден
|
||||||
глазами. Цена ошибки в демоне выше — грабля с `SIGPIPE` (см. конец §5)
|
глазами. Цена ошибки в демоне выше — грабля с `SIGPIPE` (см. конец §5)
|
||||||
убивала именно `ewsdrd`, целиком и молча.
|
убивала именно `ewsdrd`, целиком и молча.
|
||||||
4. **Проверка на железе и с настоящим клиентом** — главное, см. конец §5.
|
3. **`TX_FOOTSWITCH`** — новый `rf`-эвент контроллера и строчка в адаптере,
|
||||||
|
см. §3.1 (флаг `FHWPTTActive` уже есть, наружу не выведен).
|
||||||
|
4. **Проверка `KEYER` на железе** — код прошёл стенд, живого ключа по сети ещё
|
||||||
|
не было.
|
||||||
|
|
||||||
## 5. Проверено
|
## 5. Проверено
|
||||||
|
|
||||||
@@ -962,7 +999,7 @@ ExpertSDR3 давно бы не было.
|
|||||||
движков и сети валится с AV — клиент получает `tci_error`, соединение живо) и
|
движков и сети валится с AV — клиент получает `tci_error`, соединение живо) и
|
||||||
неразрывность пачки инициализации под крутящейся ручкой.
|
неразрывность пачки инициализации под крутящейся ручкой.
|
||||||
|
|
||||||
### Стенд этапа 2 (бинарные потоки) — 219 проверок, все зелёные
|
### Стенд этапа 2 (бинарные потоки) — 238 проверок, все зелёные
|
||||||
|
|
||||||
Отдельная программа (`test/tci/tcitest.pas`, прогон — `test/tci/run.sh`,
|
Отдельная программа (`test/tci/tcitest.pas`, прогон — `test/tci/run.sh`,
|
||||||
внешних библиотек не требует) проверяет потоки на четырёх уровнях:
|
внешних библиотек не требует) проверяет потоки на четырёх уровнях:
|
||||||
@@ -1006,6 +1043,12 @@ ExpertSDR3 давно бы не было.
|
|||||||
просьбы отбрасывается — абсолютный путь, `..` и буква диска наружу не
|
просьбы отбрасывается — абсолютный путь, `..` и буква диска наружу не
|
||||||
выводят; пусто, `..`, не-`.wav`, управляющий символ и отсутствие каталога
|
выводят; пусто, `..`, не-`.wav`, управляющий символ и отсутствие каталога
|
||||||
записей дают отказ; существующий файл писатель не перезаписывает.
|
записей дают отказ; существующий файл писатель не перезаписывает.
|
||||||
|
- **Чужая манипуляция** (`TCWElemPlayer`, отдельная часть стенда): интервалы
|
||||||
|
играются подряд и **их длительности сохраняются** (посылка 150, пауза 60,
|
||||||
|
посылка 150 — по фронтам ключа с допуском 30 мс на планировщик), на пустой
|
||||||
|
очереди ключ отпускается, следующая пачка после простоя играется целиком (а
|
||||||
|
не «догоняет» старый дедлайн), обрыв гасит очередь и отпускает ключ, нулевая
|
||||||
|
длительность не порождает ни одного фронта.
|
||||||
- **Команды на живом сервере** (настоящий `TRadioController`, WS-клиент на
|
- **Команды на живом сервере** (настоящий `TRadioController`, WS-клиент на
|
||||||
сыром сокете): отказ на несуществующий приёмник и на нечисловой аргумент,
|
сыром сокете): отказ на несуществующий приёмник и на нечисловой аргумент,
|
||||||
отказ на старт потока с незапущенного пана, подтверждение и отбраковка
|
отказ на старт потока с незапущенного пана, подтверждение и отбраковка
|
||||||
@@ -1019,7 +1062,10 @@ ExpertSDR3 давно бы не было.
|
|||||||
чужой бинарный блок не рвёт соединение; без передачи маркеров `TX_CHRONO`
|
чужой бинарный блок не рвёт соединение; без передачи маркеров `TX_CHRONO`
|
||||||
нет. Номер передатчика (§2.7): `trx:9`, `trx:abc` и `tune:9` отвечают ошибкой
|
нет. Номер передатчика (§2.7): `trx:9`, `trx:abc` и `tune:9` отвечают ошибкой
|
||||||
и **не поднимают эфир**, приёмник без живого пана — тоже; ответ называет
|
и **не поднимают эфир**, приёмник без живого пана — тоже; ответ называет
|
||||||
передающий приёмник; `TUNE` через TCI включается и гасится, не оставляя за
|
передающий приёмник; у `KEYER` номер разбирается так же (`keyer:9`,
|
||||||
|
`keyer:abc`, незапущенный приёмник и нечисловое состояние — ошибка), первое
|
||||||
|
нажатие (`arg3 = 0`) не ошибка и ничего не играет, а `keyer:0,true,<мс>`
|
||||||
|
ключ **не** замыкает (это пауза) в отличие от `keyer:0,false,<мс>`; `TUNE` через TCI включается и гасится, не оставляя за
|
||||||
собой ни тона, ни поднятой PTT. Чужая передача: поверх MOX оператора `TRX`
|
собой ни тона, ни поднятой PTT. Чужая передача: поверх MOX оператора `TRX`
|
||||||
не уводит микрофон и не снимает передачу, `TUNE` не включает тон, а уход
|
не уводит микрофон и не снимает передачу, `TUNE` не включает тон, а уход
|
||||||
такого клиента передачу оператора не гасит. Конкурирующий TCI-клиент
|
такого клиента передачу оператора не гасит. Конкурирующий TCI-клиент
|
||||||
|
|||||||
+179
-1
@@ -28,7 +28,7 @@ program tcitest;
|
|||||||
uses
|
uses
|
||||||
cthreads, Classes, SysUtils, Math, SyncObjs, Sockets, BaseUnix,
|
cthreads, Classes, SysUtils, Math, SyncObjs, Sockets, BaseUnix,
|
||||||
TCIProtocol, TCIStreams, TCIServer, TCIAdapter, RadioController,
|
TCIProtocol, TCIStreams, TCIServer, TCIAdapter, RadioController,
|
||||||
WsClient, WebUtils, WDSPEngine, Settings;
|
WsClient, WebUtils, WDSPEngine, Settings, CWMorse;
|
||||||
|
|
||||||
var
|
var
|
||||||
Passed, Failed: Integer;
|
Passed, Failed: Integer;
|
||||||
@@ -1005,6 +1005,123 @@ begin
|
|||||||
Big := nil;
|
Big := nil;
|
||||||
end;
|
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. Живой сервер: команды потоков
|
D. Живой сервер: команды потоков
|
||||||
═══════════════════════════════════════════════════════════════════════════ }
|
═══════════════════════════════════════════════════════════════════════════ }
|
||||||
@@ -1257,6 +1374,7 @@ var
|
|||||||
Cfg: TTCISettings;
|
Cfg: TTCISettings;
|
||||||
C, C2: TRawClient;
|
C, C2: TRawClient;
|
||||||
S: string;
|
S: string;
|
||||||
|
WasConn, WasRun: Boolean;
|
||||||
Blk: array[0..1023] of Byte;
|
Blk: array[0..1023] of Byte;
|
||||||
H: TTCIStreamHeader;
|
H: TTCIStreamHeader;
|
||||||
Op: Byte;
|
Op: Byte;
|
||||||
@@ -1464,6 +1582,65 @@ begin
|
|||||||
Check('tune:9 → ошибка', Pos('bad receiver', S) > 0, S);
|
Check('tune:9 → ошибка', Pos('bad receiver', S) > 0, S);
|
||||||
Check('tune:9 не включил TUN', not Ctrl.FTuning);
|
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.
|
// передачи — главный VFO, то есть 0.
|
||||||
C.SendText('trx:0;');
|
C.SendText('trx:0;');
|
||||||
@@ -1938,6 +2115,7 @@ begin
|
|||||||
TestResampling;
|
TestResampling;
|
||||||
TestStreamOut;
|
TestStreamOut;
|
||||||
TestRecorder;
|
TestRecorder;
|
||||||
|
TestKeyPlayer;
|
||||||
TestServer;
|
TestServer;
|
||||||
TestEndToEnd;
|
TestEndToEnd;
|
||||||
WriteLn;
|
WriteLn;
|
||||||
|
|||||||
Reference in New Issue
Block a user