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