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:
@@ -215,6 +215,7 @@ type
|
||||
procedure SyncSetCWDelay;
|
||||
procedure SyncCWSend;
|
||||
procedure SyncCWStop;
|
||||
procedure SyncKeyerElement;
|
||||
procedure SyncFocus;
|
||||
procedure SyncTaps; // навесить/снять тапы аудио и IQ
|
||||
|
||||
@@ -334,6 +335,7 @@ type
|
||||
// ── Отдельные команды (чтобы HandleCommand не превратился в простыню) ──
|
||||
procedure CmdFreq(Client: TTCIClient; const M: TTCIMessage; IsIF: Boolean);
|
||||
procedure CmdCWMacros(const M: TTCIMessage; IsMsg: Boolean);
|
||||
procedure CmdKeyer(Client: TTCIClient; const M: TTCIMessage);
|
||||
procedure CmdSpot(const M: TTCIMessage);
|
||||
public
|
||||
constructor Create(AController: TRadioController; ASpots: TDXSpotStore = nil);
|
||||
@@ -1673,6 +1675,12 @@ begin
|
||||
FController.CWXAbort;
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.SyncKeyerElement;
|
||||
// Поток контроллера. FsBool — посылка (иначе пауза), FsInt — её длительность.
|
||||
begin
|
||||
FController.CWKeyerElement(FsBool, FsInt);
|
||||
end;
|
||||
|
||||
procedure TTCIAdapter.SyncFocus;
|
||||
// SET_IN_FOCUS: поднять окно программы. Само окно адаптеру недоступно (он
|
||||
// равноправный клиент контроллера) — действие ставит UI через OnFocusRequest.
|
||||
@@ -1785,6 +1793,64 @@ begin
|
||||
FServer.Broadcast(TCIBuild('callsign_send', [Call]));
|
||||
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);
|
||||
// SPOT:позывной,мода,частота,цвет ARGB,текст;
|
||||
var
|
||||
@@ -2472,6 +2538,7 @@ begin
|
||||
Exit;
|
||||
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_MSG' then begin CmdCWMacros(M, True); Exit; end;
|
||||
if M.Name = 'CW_MACROS_STOP' then
|
||||
|
||||
Reference in New Issue
Block a user