mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
fix(tci): callsign_send слал размноженный позывной, tci_error — неэкранированное имя
Два расхождения со спекой, оба в том, что уходит клиенту. callsign_send (§3.2.2). Команда должна нести «финальный вариант позывного, переданного в эфир», а несла его вместе с повторами: для «cw_msg:0,_,RA6LH$2, 599 004;» уезжало «callsign_send:RA6LH RA6LH;», потому что переменная Call к этому моменту уже была затёрта развёрнутым текстом сообщения. Теперь позывной запоминается ДО развёртки. Заодно: подтверждение уходит АВТОРУ сообщения, а не broadcast'ом (у остальных клиентов своих сообщений нет), и через TCIEscape — позывной пришёл от клиента, и символы ^ ~ * после снятия экранирования превращались в сырые : , ; прямо посреди кадра. Момент отправки остаётся расхождением, теперь честно описанным в doc/TCI.md: документ шлёт команду по факту окончания передачи позывного, а наш передатчик текста моментов внутри очереди не отмечает. Доотправка позывного (cw_msg:arg1;) всё равно не поддержана, так что финальный вариант известен уже в момент постановки в очередь и позже не изменится. tci_error. Имя команды возвращается клиенту как ПЕРВЫЙ АРГУМЕНТ, то есть обязано экранироваться, — и общий обработчик (HandleCommand) это делает, а пятнадцать точечных отказов пропускали его сырым. Имя приходит от клиента, TCIParse режет его только по ':', так что «foo,bar:1;» возвращался как «tci_error:foo,bar,bad receiver;» — три аргумента вместо двух. Заодно в doc/TCI.md: правило 3 §1.1 теперь говорит про ВСЕ потоки сервера (accept и тик тоже, общий JoinPumped), а не только про клиентские, и счётчик проверок стенда приведён к нынешним 244. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+39
-23
@@ -334,7 +334,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(Client: TTCIClient; const M: TTCIMessage; IsMsg: Boolean);
|
||||||
procedure CmdKeyer(Client: TTCIClient; const M: TTCIMessage);
|
procedure CmdKeyer(Client: TTCIClient; const M: TTCIMessage);
|
||||||
procedure CmdSpot(const M: TTCIMessage);
|
procedure CmdSpot(const M: TTCIMessage);
|
||||||
public
|
public
|
||||||
@@ -1728,14 +1728,15 @@ begin
|
|||||||
if IsIF then Reply(Client, StrIf(Rx, Ch)) else Reply(Client, StrVfo(Rx, Ch));
|
if IsIF then Reply(Client, StrIf(Rx, Ch)) else Reply(Client, StrVfo(Rx, Ch));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTCIAdapter.CmdCWMacros(const M: TTCIMessage; IsMsg: Boolean);
|
procedure TTCIAdapter.CmdCWMacros(Client: TTCIClient; const M: TTCIMessage;
|
||||||
|
IsMsg: Boolean);
|
||||||
// CW_MACROS:trx,текст; CW_MSG:trx,префикс,позывной,суффикс;
|
// CW_MACROS:trx,текст; CW_MSG:trx,префикс,позывной,суффикс;
|
||||||
// Разметку TCI приводим к тому, что понимает передатчик текста ewsdr:
|
// Разметку TCI приводим к тому, что понимает передатчик текста ewsdr:
|
||||||
// |ABBR| — слитная передача (у нас такой команды нет) → скобки снимаем;
|
// |ABBR| — слитная передача (у нас такой команды нет) → скобки снимаем;
|
||||||
// < / > — шаг скорости ±5 wpm (передатчик работает на одной скорости) → снимаем;
|
// < / > — шаг скорости ±5 wpm (передатчик работает на одной скорости) → снимаем;
|
||||||
// CALL$N — повтор позывного N раз.
|
// CALL$N — повтор позывного N раз.
|
||||||
var
|
var
|
||||||
Text, Prefix, Call, Suffix, Rep: string;
|
Text, Prefix, Call, Suffix, Rep, Final: string;
|
||||||
P, N, i: Integer;
|
P, N, i: Integer;
|
||||||
begin
|
begin
|
||||||
if IsMsg then
|
if IsMsg then
|
||||||
@@ -1749,9 +1750,13 @@ begin
|
|||||||
if Prefix = '_' then Prefix := '';
|
if Prefix = '_' then Prefix := '';
|
||||||
if Suffix = '_' then Suffix := '';
|
if Suffix = '_' then Suffix := '';
|
||||||
|
|
||||||
|
// Финальный вариант позывного — ДО развёртки повторов: клиенту он нужен
|
||||||
|
// как позывной (§3.2.2), а не как «RA6LH RA6LH».
|
||||||
|
Final := Call;
|
||||||
P := Pos('$', Call);
|
P := Pos('$', Call);
|
||||||
if P > 0 then
|
if P > 0 then
|
||||||
begin
|
begin
|
||||||
|
Final := Trim(Copy(Call, 1, P - 1));
|
||||||
Rep := Copy(Call, P + 1, MaxInt);
|
Rep := Copy(Call, P + 1, MaxInt);
|
||||||
Call := Copy(Call, 1, P - 1);
|
Call := Copy(Call, 1, P - 1);
|
||||||
N := StrToIntDef(Trim(Rep), 1);
|
N := StrToIntDef(Trim(Rep), 1);
|
||||||
@@ -1788,9 +1793,14 @@ begin
|
|||||||
FLock.Leave;
|
FLock.Leave;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Позывной ушёл в эфир целиком — подтверждаем финальный вариант (§3.2.2).
|
// §3.2.2: подтверждение финального варианта позывного — АВТОРУ сообщения, а
|
||||||
if IsMsg and (Call <> '') then
|
// не всем (у остальных клиентов своих сообщений нет). ★Расхождение со спекой,
|
||||||
FServer.Broadcast(TCIBuild('callsign_send', [Call]));
|
// осознанное: документ шлёт эту команду по факту окончания передачи позывного,
|
||||||
|
// а наш передатчик текста моментов внутри очереди не отмечает — подтверждаем
|
||||||
|
// сразу. Доотправка позывного (cw_msg:arg1;) всё равно не поддержана, так что
|
||||||
|
// финальный вариант известен уже здесь и позже не изменится.
|
||||||
|
if IsMsg and (Final <> '') then
|
||||||
|
Reply(Client, TCIBuild('callsign_send', [TCIEscape(Final)]));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTCIAdapter.CmdKeyer(Client: TTCIClient; const M: TTCIMessage);
|
procedure TTCIAdapter.CmdKeyer(Client: TTCIClient; const M: TTCIMessage);
|
||||||
@@ -1814,19 +1824,20 @@ var
|
|||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
|
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
|
||||||
begin
|
begin
|
||||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'bad receiver']));
|
Reply(Client, TCIBuild('tci_error',
|
||||||
|
[TCIEscape(LowerCase(M.Name)), 'bad receiver']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
// Номер объявлен потолком железа, но пана под ним может не быть — как у TRX.
|
// Номер объявлен потолком железа, но пана под ним может не быть — как у TRX.
|
||||||
if (Rx > 0) and not RxActive(Rx) then
|
if (Rx > 0) and not RxActive(Rx) then
|
||||||
begin
|
begin
|
||||||
Reply(Client, TCIBuild('tci_error',
|
Reply(Client, TCIBuild('tci_error',
|
||||||
[LowerCase(M.Name), 'receiver is not running']));
|
[TCIEscape(LowerCase(M.Name)), 'receiver is not running']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
if not TCITryArgBool(M, 1, Down) then
|
if not TCITryArgBool(M, 1, Down) then
|
||||||
begin
|
begin
|
||||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'bad state']));
|
Reply(Client, TCIBuild('tci_error', [TCIEscape(LowerCase(M.Name)), 'bad state']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
if not TCITryArgInt(M, 2, Ms) then Ms := 0;
|
if not TCITryArgInt(M, 2, Ms) then Ms := 0;
|
||||||
@@ -2001,7 +2012,8 @@ begin
|
|||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
|
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
|
||||||
begin
|
begin
|
||||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'bad receiver']));
|
Reply(Client, TCIBuild('tci_error',
|
||||||
|
[TCIEscape(LowerCase(M.Name)), 'bad receiver']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
// Номер объявлен (TRX_COUNT = потолок железа), но пана под ним может не
|
// Номер объявлен (TRX_COUNT = потолок железа), но пана под ним может не
|
||||||
@@ -2010,7 +2022,7 @@ begin
|
|||||||
if (Rx > 0) and not RxActive(Rx) then
|
if (Rx > 0) and not RxActive(Rx) then
|
||||||
begin
|
begin
|
||||||
Reply(Client, TCIBuild('tci_error',
|
Reply(Client, TCIBuild('tci_error',
|
||||||
[LowerCase(M.Name), 'receiver is not running']));
|
[TCIEscape(LowerCase(M.Name)), 'receiver is not running']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
// Ключ захвата (§3.5) — общий на радио, а не на приёмник: передатчик один,
|
// Ключ захвата (§3.5) — общий на радио, а не на приёмник: передатчик один,
|
||||||
@@ -2539,8 +2551,8 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
if M.Name = 'KEYER' then begin CmdKeyer(Client, M); 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_MACROS' then begin CmdCWMacros(Client, M, False); Exit; end;
|
||||||
if M.Name = 'CW_MSG' then begin CmdCWMacros(M, True); Exit; end;
|
if M.Name = 'CW_MSG' then begin CmdCWMacros(Client, M, True); Exit; end;
|
||||||
if M.Name = 'CW_MACROS_STOP' then
|
if M.Name = 'CW_MACROS_STOP' then
|
||||||
begin
|
begin
|
||||||
FLock.Enter;
|
FLock.Enter;
|
||||||
@@ -3066,7 +3078,7 @@ begin
|
|||||||
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
|
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
|
||||||
begin
|
begin
|
||||||
Reply(Client, TCIBuild('tci_error',
|
Reply(Client, TCIBuild('tci_error',
|
||||||
[LowerCase(M.Name), 'bad receiver']));
|
[TCIEscape(LowerCase(M.Name)), 'bad receiver']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -3086,7 +3098,7 @@ begin
|
|||||||
if not RxActive(Rx) then
|
if not RxActive(Rx) then
|
||||||
begin
|
begin
|
||||||
Reply(Client, TCIBuild('tci_error',
|
Reply(Client, TCIBuild('tci_error',
|
||||||
[LowerCase(M.Name), 'receiver is not running']));
|
[TCIEscape(LowerCase(M.Name)), 'receiver is not running']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
StartStream(Client, K, Rx);
|
StartStream(Client, K, Rx);
|
||||||
@@ -3108,7 +3120,8 @@ var
|
|||||||
begin
|
begin
|
||||||
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
|
if not TCITryArgInt(M, 0, Rx) or not ValidRx(Rx) then
|
||||||
begin
|
begin
|
||||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'bad receiver']));
|
Reply(Client, TCIBuild('tci_error',
|
||||||
|
[TCIEscape(LowerCase(M.Name)), 'bad receiver']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -3120,7 +3133,7 @@ begin
|
|||||||
if not RxActive(Rx) then
|
if not RxActive(Rx) then
|
||||||
begin
|
begin
|
||||||
Reply(Client, TCIBuild('tci_error',
|
Reply(Client, TCIBuild('tci_error',
|
||||||
[LowerCase(M.Name), 'receiver is not running']));
|
[TCIEscape(LowerCase(M.Name)), 'receiver is not running']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
if not TCITryArgInt(M, 1, Sec) then Sec := TCI_RECORD_MAX_SEC;
|
if not TCITryArgInt(M, 1, Sec) then Sec := TCI_RECORD_MAX_SEC;
|
||||||
@@ -3159,7 +3172,8 @@ begin
|
|||||||
Req := TCIUnescape(TCIArg(M, 1));
|
Req := TCIUnescape(TCIArg(M, 1));
|
||||||
if Req = '' then
|
if Req = '' then
|
||||||
begin
|
begin
|
||||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'no file name']));
|
Reply(Client, TCIBuild('tci_error',
|
||||||
|
[TCIEscape(LowerCase(M.Name)), 'no file name']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
// MP3 у нас кодировать нечем — молча подсунуть WAV с расширением .mp3 хуже,
|
// MP3 у нас кодировать нечем — молча подсунуть WAV с расширением .mp3 хуже,
|
||||||
@@ -3168,21 +3182,22 @@ begin
|
|||||||
if SameText(ExtractFileExt(Req), '.mp3') then
|
if SameText(ExtractFileExt(Req), '.mp3') then
|
||||||
begin
|
begin
|
||||||
Reply(Client, TCIBuild('tci_error',
|
Reply(Client, TCIBuild('tci_error',
|
||||||
[LowerCase(M.Name), 'only wav is supported']));
|
[TCIEscape(LowerCase(M.Name)), 'only wav is supported']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
// ★Каталог всегда наш, из просьбы берётся одно имя файла — см. TCIRecordPath.
|
// ★Каталог всегда наш, из просьбы берётся одно имя файла — см. TCIRecordPath.
|
||||||
Path := TCIRecordPath(RecordDir, Req);
|
Path := TCIRecordPath(RecordDir, Req);
|
||||||
if Path = '' then
|
if Path = '' then
|
||||||
begin
|
begin
|
||||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'bad file name']));
|
Reply(Client, TCIBuild('tci_error',
|
||||||
|
[TCIEscape(LowerCase(M.Name)), 'bad file name']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
// Существующий файл не трогаем (окончательно это решит O_EXCL в писателе,
|
// Существующий файл не трогаем (окончательно это решит O_EXCL в писателе,
|
||||||
// здесь — только чтобы клиент услышал причину).
|
// здесь — только чтобы клиент услышал причину).
|
||||||
if FileExists(Path) then
|
if FileExists(Path) then
|
||||||
begin
|
begin
|
||||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'file exists']));
|
Reply(Client, TCIBuild('tci_error', [TCIEscape(LowerCase(M.Name)), 'file exists']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -3216,7 +3231,8 @@ begin
|
|||||||
// тут же и пишется), но возвращаем на всякий случай: единственный путь,
|
// тут же и пишется), но возвращаем на всякий случай: единственный путь,
|
||||||
// на котором запись не доходит до писателя.
|
// на котором запись не доходит до писателя.
|
||||||
TCIRecBudgetFree(Rec.Reserved);
|
TCIRecBudgetFree(Rec.Reserved);
|
||||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'nothing recorded']));
|
Reply(Client, TCIBuild('tci_error',
|
||||||
|
[TCIEscape(LowerCase(M.Name)), 'nothing recorded']));
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
// Пишет отдельный поток: файл может быть в десятки мегабайт, а мы сейчас в
|
// Пишет отдельный поток: файл может быть в десятки мегабайт, а мы сейчас в
|
||||||
@@ -3228,7 +3244,7 @@ begin
|
|||||||
// Очередь полна (медленный диск) — запись отдать некому, значит и её
|
// Очередь полна (медленный диск) — запись отдать некому, значит и её
|
||||||
// место в бюджете держать больше незачем.
|
// место в бюджете держать больше незачем.
|
||||||
TCIRecBudgetFree(Rec.Reserved);
|
TCIRecBudgetFree(Rec.Reserved);
|
||||||
Reply(Client, TCIBuild('tci_error', [LowerCase(M.Name), 'writer busy']));
|
Reply(Client, TCIBuild('tci_error', [TCIEscape(LowerCase(M.Name)), 'writer busy']));
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
+15
-4
@@ -85,10 +85,15 @@ TCI-клиенты ──WebSocket──► TTCIServer ──► TTCIAdapter ─
|
|||||||
том и в другом случае наверх уходит `OnDisconnect` (единая точка
|
том и в другом случае наверх уходит `OnDisconnect` (единая точка
|
||||||
`Disconnected`): иначе после остановки у адаптера оставались висеть захваты
|
`Disconnected`): иначе после остановки у адаптера оставались висеть захваты
|
||||||
параметров ушедших клиентов.
|
параметров ушедших клиентов.
|
||||||
3. **`Stop` ждёт выхода клиентских потоков без таймаута** и прокачивает при
|
3. **`Stop` ждёт выхода ВСЕХ своих потоков без таймаута** и прокачивает при
|
||||||
этом очередь `Synchronize`. Останавливает сервер поток контроллера (UI), а
|
этом очередь `Synchronize`. Останавливает сервер поток контроллера (UI), а
|
||||||
клиентский поток в этот момент может висеть как раз на `Invoke` в него же:
|
клиентский поток в этот момент может висеть как раз на `Invoke` в него же:
|
||||||
без прокачки это взаимный клин. Выйти по таймауту нельзя — следом
|
без прокачки это взаимный клин. ★Это относится и к accept- с тик-потоком
|
||||||
|
(общий `JoinPumped`), а не только к клиентским: тик-поток ходит в контроллер
|
||||||
|
через `ReapClients` → `Disconnected` → `Invoke`, и проверка `CanInvoke` от
|
||||||
|
клина не спасает — она читает `Stopping` ДО входа в `Synchronize`, так что
|
||||||
|
клиент, отвалившийся ровно в момент остановки, успевает проскочить в это
|
||||||
|
окно. Выйти по таймауту нельзя — следом
|
||||||
освобождаются и клиенты, и сам сервер с адаптером, а не вышедший поток
|
освобождаются и клиенты, и сам сервер с адаптером, а не вышедший поток
|
||||||
вернулся бы в эту память. Поэтому: `Stopping` (адаптер новых `Invoke` не
|
вернулся бы в эту память. Поэтому: `Stopping` (адаптер новых `Invoke` не
|
||||||
начинает) + закрытые сокеты + повторный `shutdown` раз в полсекунды, и
|
начинает) + закрытые сокеты + повторный `shutdown` раз в полсекунды, и
|
||||||
@@ -703,7 +708,13 @@ ExpertSDR3 давно бы не было.
|
|||||||
|
|
||||||
Текст приводится к тому, что понимает передатчик текста ewsdr (`CWXSend`):
|
Текст приводится к тому, что понимает передатчик текста ewsdr (`CWXSend`):
|
||||||
экранирование `^ ~ *` снимается, `CALL$N` разворачивается в N повторов
|
экранирование `^ ~ *` снимается, `CALL$N` разворачивается в N повторов
|
||||||
позывного, префикс/суффикс `_` считаются пустыми. **Не поддержано:** шаг
|
позывного, префикс/суффикс `_` считаются пустыми. Подтверждение
|
||||||
|
`callsign_send` уходит **автору сообщения** и содержит позывной ДО развёртки
|
||||||
|
повторов (`RA6LH`, а не «RA6LH RA6LH»). ★Расхождение со спекой, осознанное:
|
||||||
|
документ шлёт эту команду по факту окончания передачи позывного, а наш
|
||||||
|
передатчик текста моментов внутри очереди не отмечает — подтверждаем сразу.
|
||||||
|
Доотправка всё равно не поддержана, так что финальный вариант известен уже
|
||||||
|
здесь и позже не изменится. **Не поддержано:** шаг
|
||||||
скорости внутри текста (`<` / `>`) и слитная передача аббревиатур (`|SK|`) —
|
скорости внутри текста (`<` / `>`) и слитная передача аббревиатур (`|SK|`) —
|
||||||
эти символы просто снимаются, потому что `TCWSender` работает на одной
|
эти символы просто снимаются, потому что `TCWSender` работает на одной
|
||||||
скорости и не знает прос-знаков. Доотправка позывного (`cw_msg:arg1;`)
|
скорости и не знает прос-знаков. Доотправка позывного (`cw_msg:arg1;`)
|
||||||
@@ -999,7 +1010,7 @@ running`), а сам телеграф уходит **текущему TX-ист
|
|||||||
движков и сети валится с AV — клиент получает `tci_error`, соединение живо) и
|
движков и сети валится с AV — клиент получает `tci_error`, соединение живо) и
|
||||||
неразрывность пачки инициализации под крутящейся ручкой.
|
неразрывность пачки инициализации под крутящейся ручкой.
|
||||||
|
|
||||||
### Стенд этапа 2 (бинарные потоки) — 238 проверок, все зелёные
|
### Стенд этапа 2 (бинарные потоки) — 244 проверки, все зелёные
|
||||||
|
|
||||||
Отдельная программа (`test/tci/tcitest.pas`, прогон — `test/tci/run.sh`,
|
Отдельная программа (`test/tci/tcitest.pas`, прогон — `test/tci/run.sh`,
|
||||||
внешних библиотек не требует) проверяет потоки на четырёх уровнях:
|
внешних библиотек не требует) проверяет потоки на четырёх уровнях:
|
||||||
|
|||||||
Reference in New Issue
Block a user