mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
fix(tci): пара «клиент+приёмник» только по факту передачи; перебор имён .part
[P1] FTxClient/FTxRx писались ДО Invoke, и команда, которая ничего не сделала,
всё равно их перебивала. Клиент, уже передающий с приёмника 1, шлёт
trx:0,true,tci — передатчик занят, SyncSetTRX не делает ничего (Started=False),
а маркеры ИДУЩЕЙ передачи с этого мига уходят под номером 0. MSHV такие блоки
отбрасывает (network.cpp:231), то есть передача просто замолкает. Теперь пара
назначается после Invoke и только при Started — тем же признаком, по которому
назначается хозяин эфира. Тот же гейт закрывает близнеца: trx:<N>,true без
',tci' поверх своей же передачи больше не снимает источник модуляции. Снятие
(',false') работает как прежде.
[P2] Имя временного файла (pid + счётчик) уникально внутри процесса, но не
между запусками: «.part», оставшийся от прошлой жизни (публиковать было
нечем), плюс повторно выданный системой pid дают EEXIST на создании — и
задание пропадало молча. TCICreateTempNear перебирает до 64 имён, но только
пока ошибка — «имя занято»: нет прав или каталога перебором не лечится.
Стенд test/tci: 219 проверок (было 217). Новое — занятое имя «.part» записи не
теряет (стенд занимает ровно то имя, которое возьмёт писатель) и пустая
команда TRX не меняет номер приёмника в маркерах. Негативный контроль на обе
правки. Попутно в тесте поправлены два комментария, описывавшие прежнюю
реализацию публикации.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+29
-8
@@ -1962,14 +1962,6 @@ begin
|
|||||||
Tune := M.Name = 'TUNE';
|
Tune := M.Name = 'TUNE';
|
||||||
Name := LowerCase(Trim(TCIArg(M, 2)));
|
Name := LowerCase(Trim(TCIArg(M, 2)));
|
||||||
FromTCI := B and (not Tune) and (Name = 'tci') and HasAudioStream(Client);
|
FromTCI := B and (not Tune) and (Name = 'tci') and HasAudioStream(Client);
|
||||||
FTxLock.Enter;
|
|
||||||
try
|
|
||||||
// ★Вместе с клиентом запоминаем НОМЕР ПРИЁМНИКА, которым он назвался:
|
|
||||||
// маркеры TX_CHRONO обязаны идти под ним же (см. PushTxChrono).
|
|
||||||
if FromTCI then begin FTxClient := Client; FTxRx := Rx; end
|
|
||||||
else if (not Tune) and (FTxClient = Client) then
|
|
||||||
begin FTxClient := nil; FTxRx := 0; end;
|
|
||||||
finally FTxLock.Leave; end;
|
|
||||||
Started := False;
|
Started := False;
|
||||||
FLock.Enter;
|
FLock.Enter;
|
||||||
try
|
try
|
||||||
@@ -1991,6 +1983,35 @@ begin
|
|||||||
try
|
try
|
||||||
if Started then FTrxOwner := Client
|
if Started then FTrxOwner := Client
|
||||||
else if (not B) and (FTrxOwner = Client) then FTrxOwner := nil;
|
else if (not B) and (FTrxOwner = Client) then FTrxOwner := nil;
|
||||||
|
// Источник модуляции и НОМЕР ПРИЁМНИКА, под которым идут маркеры
|
||||||
|
// TX_CHRONO (см. PushTxChrono), — по тому же правилу: меняем их
|
||||||
|
// ТОЛЬКО когда команда и правда что-то сделала.
|
||||||
|
// ★Раньше пара «клиент+приёмник» писалась до Invoke, и команда, которая
|
||||||
|
// ничего не изменила, всё равно её перебивала: клиент, уже
|
||||||
|
// передававший с приёмника 1, присылает trx:0,true,tci — передатчик
|
||||||
|
// занят, SyncSetTRX не делает ничего, а маркеры его же идущей передачи
|
||||||
|
// с этого мига уходят под номером 0. MSHV такие блоки отбрасывает
|
||||||
|
// (network.cpp:231), то есть передача просто замолкает.
|
||||||
|
if B then
|
||||||
|
begin
|
||||||
|
if Started and FromTCI then
|
||||||
|
begin
|
||||||
|
FTxClient := Client;
|
||||||
|
FTxRx := Rx;
|
||||||
|
end
|
||||||
|
// Эфир поднял он же, но модуляцию просит НЕ из TCI — снимаем свою.
|
||||||
|
else if Started and (not Tune) and (FTxClient = Client) then
|
||||||
|
begin
|
||||||
|
FTxClient := nil;
|
||||||
|
FTxRx := 0;
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
// Сам просит выключить — источник ему больше не нужен в любом случае.
|
||||||
|
else if (not Tune) and (FTxClient = Client) then
|
||||||
|
begin
|
||||||
|
FTxClient := nil;
|
||||||
|
FTxRx := 0;
|
||||||
|
end;
|
||||||
finally FTxLock.Leave; end;
|
finally FTxLock.Leave; end;
|
||||||
end;
|
end;
|
||||||
// ★Автору отвечаем ЕГО номером приёмника (см. StrTrx): клиент фильтрует
|
// ★Автору отвечаем ЕГО номером приёмника (см. StrTrx): клиент фильтрует
|
||||||
|
|||||||
+38
-3
@@ -353,6 +353,11 @@ function TCIRecordPath(const BaseDir, Req: string): string;
|
|||||||
имя приходит из сети. THandle(-1) — не вышло. }
|
имя приходит из сети. THandle(-1) — не вышло. }
|
||||||
function TCICreateNewFile(const Path: string): THandle;
|
function TCICreateNewFile(const Path: string): THandle;
|
||||||
|
|
||||||
|
{ Очередное имя временного файла рядом с целью: <путь>.<pid>-<N>.part. Каждый
|
||||||
|
вызов даёт следующее. Наружу открыто ради стенда — ему нужно занять ровно то
|
||||||
|
имя, которое возьмёт писатель. }
|
||||||
|
function TCIRecTempName(const Path: string): string;
|
||||||
|
|
||||||
{ Погасить писателя: закрыть приём заданий, дождаться ВСЕЙ очереди и
|
{ Погасить писателя: закрыть приём заданий, дождаться ВСЕЙ очереди и
|
||||||
освободить объект (Destroy = Terminate + WaitFor). Зовут при гибели
|
освободить объект (Destroy = Terminate + WaitFor). Зовут при гибели
|
||||||
адаптера, ради этого писатель и стал управляемым. W обнуляется в любом
|
адаптера, ради этого писатель и стал управляемым. W обнуляется в любом
|
||||||
@@ -972,6 +977,37 @@ begin
|
|||||||
[Path, Integer(GetProcessID), InterLockedIncrement(RecTmpSeq)]);
|
[Path, Integer(GetProcessID), InterLockedIncrement(RecTmpSeq)]);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TCILastErrorIsExists: Boolean;
|
||||||
|
// «Такое имя уже есть» — единственная ошибка создания, которую лечит другое имя.
|
||||||
|
begin
|
||||||
|
{$IFDEF UNIX}
|
||||||
|
Result := fpGetErrno = ESysEEXIST;
|
||||||
|
{$ELSE}
|
||||||
|
Result := (GetLastError = ERROR_ALREADY_EXISTS) or
|
||||||
|
(GetLastError = ERROR_FILE_EXISTS);
|
||||||
|
{$ENDIF}
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCICreateTempNear(const Path: string; out Tmp: string): THandle;
|
||||||
|
// Временный файл рядом с целью, с перебором имён.
|
||||||
|
// ★pid + счётчик уникальны внутри процесса, но не между запусками: «.part»,
|
||||||
|
// оставшийся от прошлой жизни (публиковать было нечем — см. TCIPublishFile),
|
||||||
|
// плюс повторно выданный системой pid дают EEXIST, и задание пропадало бы
|
||||||
|
// молча. Занятое имя — не повод терять запись, берём следующее.
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
for i := 1 to 64 do
|
||||||
|
begin
|
||||||
|
Tmp := TCIRecTempName(Path);
|
||||||
|
Result := TCICreateNewFile(Tmp);
|
||||||
|
if Result <> THandle(-1) then Exit;
|
||||||
|
// Не «имя занято» (нет прав, нет каталога) — перебор не поможет.
|
||||||
|
if not TCILastErrorIsExists then Break;
|
||||||
|
end;
|
||||||
|
Tmp := '';
|
||||||
|
Result := THandle(-1);
|
||||||
|
end;
|
||||||
|
|
||||||
function TCIWriteAll(H: THandle; const Buf; Count: Integer): Boolean;
|
function TCIWriteAll(H: THandle; const Buf; Count: Integer): Boolean;
|
||||||
// ★Результат записи проверяем, и не «= Count», а циклом. FileWrite (как и
|
// ★Результат записи проверяем, и не «= Count», а циклом. FileWrite (как и
|
||||||
// THandleStream.Write под ним) возвращает ЧИСЛО записанных байт и при ошибке
|
// THandleStream.Write под ним) возвращает ЧИСЛО записанных байт и при ошибке
|
||||||
@@ -1276,9 +1312,8 @@ begin
|
|||||||
// Так клиент, увидевший файл, всегда прав: раньше по имени сначала
|
// Так клиент, увидевший файл, всегда прав: раньше по имени сначала
|
||||||
// появлялась пустышка на 0 байт, и на медленном диске её было видно всю
|
// появлялась пустышка на 0 байт, и на медленном диске её было видно всю
|
||||||
// запись, а до того — огрызок с заголовком на полную длину.
|
// запись, а до того — огрызок с заголовком на полную длину.
|
||||||
Ok := False;
|
Ok := False;
|
||||||
Tmp := TCIRecTempName(J.Path);
|
HT := TCICreateTempNear(J.Path, Tmp);
|
||||||
HT := TCICreateNewFile(Tmp);
|
|
||||||
if HT <> THandle(-1) then
|
if HT <> THandle(-1) then
|
||||||
begin
|
begin
|
||||||
try
|
try
|
||||||
|
|||||||
+15
-5
@@ -503,7 +503,11 @@ TX-аудио не по своей воле, а строго в ответ на
|
|||||||
сидящий на **втором слайсе** (`tci_trx = 1`), маркеров не видел вовсе: эфир по
|
сидящий на **втором слайсе** (`tci_trx = 1`), маркеров не видел вовсе: эфир по
|
||||||
`trx:1,true,tci` поднимался, а звука не было ни одного блока. Поэтому вместе с
|
`trx:1,true,tci` поднимался, а звука не было ни одного блока. Поэтому вместе с
|
||||||
клиентом-модулятором запоминается номер приёмника из его же `TRX`
|
клиентом-модулятором запоминается номер приёмника из его же `TRX`
|
||||||
(`FTxRx`), и маркеры идут под ним.
|
(`FTxRx`), и маркеры идут под ним. ★Пара «клиент + приёмник» меняется **только
|
||||||
|
когда команда и правда что-то сделала** (`Started`, тот же признак, по которому
|
||||||
|
назначается хозяин эфира): иначе клиент, уже передающий с приёмника 1, своим же
|
||||||
|
`trx:0,true,tci` — командой-пустышкой, потому что передатчик занят, — перевёл бы
|
||||||
|
маркеры идущей передачи на номер 0 и замолчал.
|
||||||
|
|
||||||
**★`rfTransmitting` — не всегда «эфир изменился».** Контроллер шлёт это поле и
|
**★`rfTransmitting` — не всегда «эфир изменился».** Контроллер шлёт это поле и
|
||||||
просто «перерисуй TX-бейджи»: `SetTxSlice` заканчивается `Changed(rfTransmitting)`,
|
просто «перерисуй TX-бейджи»: `SetTxSlice` заканчивается `Changed(rfTransmitting)`,
|
||||||
@@ -580,7 +584,11 @@ ExpertSDR3 давно бы не было.
|
|||||||
1. данные пишутся во **временный файл** рядом (`<имя>.<pid>-<n>.part`,
|
1. данные пишутся во **временный файл** рядом (`<имя>.<pid>-<n>.part`,
|
||||||
создаётся эксклюзивно и не по симлинку — `TCICreateNewFile`), циклом, с
|
создаётся эксклюзивно и не по симлинку — `TCICreateNewFile`), циклом, с
|
||||||
проверкой каждого вызова (короткая запись законна, её дописываем; 0 или
|
проверкой каждого вызова (короткая запись законна, её дописываем; 0 или
|
||||||
-1 — провал);
|
-1 — провал). ★Имя перебирается (`TCICreateTempNear`): pid со счётчиком
|
||||||
|
уникальны внутри процесса, но не между запусками, и `.part`, оставшийся от
|
||||||
|
прошлой жизни, вместе с повторно выданным системой pid дал бы `EEXIST` —
|
||||||
|
занятое имя не повод терять запись, а вот другая ошибка (нет прав, нет
|
||||||
|
каталога) перебором не лечится и обрывает попытку сразу;
|
||||||
2. перед публикацией идёт `FileFlush`: на ext4 с отложенным размещением «нет
|
2. перед публикацией идёт `FileFlush`: на ext4 с отложенным размещением «нет
|
||||||
места» приходит не в `write`, а именно здесь;
|
места» приходит не в `write`, а именно здесь;
|
||||||
3. и только после этого файл появляется под целевым именем — **одним вызовом
|
3. и только после этого файл появляется под целевым именем — **одним вызовом
|
||||||
@@ -954,7 +962,7 @@ ExpertSDR3 давно бы не было.
|
|||||||
движков и сети валится с AV — клиент получает `tci_error`, соединение живо) и
|
движков и сети валится с AV — клиент получает `tci_error`, соединение живо) и
|
||||||
неразрывность пачки инициализации под крутящейся ручкой.
|
неразрывность пачки инициализации под крутящейся ручкой.
|
||||||
|
|
||||||
### Стенд этапа 2 (бинарные потоки) — 217 проверок, все зелёные
|
### Стенд этапа 2 (бинарные потоки) — 219 проверок, все зелёные
|
||||||
|
|
||||||
Отдельная программа (`test/tci/tcitest.pas`, прогон — `test/tci/run.sh`,
|
Отдельная программа (`test/tci/tcitest.pas`, прогон — `test/tci/run.sh`,
|
||||||
внешних библиотек не требует) проверяет потоки на четырёх уровнях:
|
внешних библиотек не требует) проверяет потоки на четырёх уровнях:
|
||||||
@@ -988,7 +996,8 @@ ExpertSDR3 давно бы не было.
|
|||||||
возвращает резерв по окончании записи. ★Писатель: задание принимается и
|
возвращает резерв по окончании записи. ★Писатель: задание принимается и
|
||||||
дописывается очередью, после удачи временного файла рядом не остаётся,
|
дописывается очередью, после удачи временного файла рядом не остаётся,
|
||||||
неудачная запись (счёт обещает больше, чем есть в кусках) не оставляет НИ
|
неудачная запись (счёт обещает больше, чем есть в кусках) не оставляет НИ
|
||||||
файла, НИ `.part`, за всё время записи 32 МБ целевое имя ни разу не видно
|
файла, НИ `.part`, занятое имя `.part` (файл от прошлого запуска с тем же
|
||||||
|
pid) записи не теряет, за всё время записи 32 МБ целевое имя ни разу не видно
|
||||||
незаконченным, сверх потолка заданий следует отказ, после `Close` заданий не
|
незаконченным, сверх потолка заданий следует отказ, после `Close` заданий не
|
||||||
берут, а `TCIStopWriter` дожидается 32-МБ записи — файл цел сразу после его
|
берут, а `TCIStopWriter` дожидается 32-МБ записи — файл цел сразу после его
|
||||||
возврата, и **хвост очереди за ней дописан тоже** (это и есть проверка на
|
возврата, и **хвост очереди за ней дописан тоже** (это и есть проверка на
|
||||||
@@ -1025,7 +1034,8 @@ ExpertSDR3 давно бы не было.
|
|||||||
аудио блоками с `receiver = 1`, ★по `trx:1,true,tci` берёт модуляцию из TCI
|
аудио блоками с `receiver = 1`, ★по `trx:1,true,tci` берёт модуляцию из TCI
|
||||||
(то есть просьбу не стирает `Changed(rfTransmitting)` из `SetTxSlice`) и шлёт
|
(то есть просьбу не стирает `Changed(rfTransmitting)` из `SetTxSlice`) и шлёт
|
||||||
маркеры `TX_CHRONO` **под номером 1** — оба этих места и ломали передачу
|
маркеры `TX_CHRONO` **под номером 1** — оба этих места и ломали передачу
|
||||||
MSHV, сидящего на втором слайсе, а после удаления слайса приёмник 1 замолкает
|
MSHV, сидящего на втором слайсе; а `trx:0,true,tci` поверх этой же передачи
|
||||||
|
(команда-пустышка) номер приёмника в маркерах не меняет, а после удаления слайса приёмник 1 замолкает
|
||||||
целиком — вместо прежнего `vfo:1,0,0`.
|
целиком — вместо прежнего `vfo:1,0,0`.
|
||||||
- **Сквозной прогон через живой WDSP:** синтетический 24-битный IQ подаётся
|
- **Сквозной прогон через живой WDSP:** синтетический 24-битный IQ подаётся
|
||||||
в движок, а клиент по WebSocket получает блоки RX-аудио 12 кГц и IQ 48 кГц
|
в движок, а клиент по WebSocket получает блоки RX-аудио 12 кГц и IQ 48 кГц
|
||||||
|
|||||||
+61
-8
@@ -595,6 +595,20 @@ begin
|
|||||||
Result := W.Pending = 0;
|
Result := W.Pending = 0;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function NextTempName(const Name: string): string;
|
||||||
|
// Имя «.part», следующее за данным: у писателя счётчик один на процесс, и
|
||||||
|
// стенду нужно занять именно то имя, которое он возьмёт после нашего вызова.
|
||||||
|
var i, p1, p2: Integer;
|
||||||
|
begin
|
||||||
|
p2 := Pos('.part', Name);
|
||||||
|
p1 := p2;
|
||||||
|
for i := p2 - 1 downto 1 do
|
||||||
|
if Name[i] = '-' then begin p1 := i; Break; end;
|
||||||
|
Result := Copy(Name, 1, p1) +
|
||||||
|
IntToStr(StrToIntDef(Copy(Name, p1 + 1, p2 - p1 - 1), 0) + 1) +
|
||||||
|
'.part';
|
||||||
|
end;
|
||||||
|
|
||||||
function PartFiles(const Path: string): Integer;
|
function PartFiles(const Path: string): Integer;
|
||||||
// Сколько временных файлов писателя осталось рядом с целью.
|
// Сколько временных файлов писателя осталось рядом с целью.
|
||||||
var SR: TSearchRec;
|
var SR: TSearchRec;
|
||||||
@@ -624,7 +638,7 @@ var
|
|||||||
Tk: TTCIRecTake;
|
Tk: TTCIRecTake;
|
||||||
Big: TTCIPcm;
|
Big: TTCIPcm;
|
||||||
Refused, k, Bad: Integer;
|
Refused, k, Bad: Integer;
|
||||||
Path2: string;
|
Path2, Squat: string;
|
||||||
begin
|
begin
|
||||||
WriteLn('C. Рекордер линейного выхода');
|
WriteLn('C. Рекордер линейного выхода');
|
||||||
|
|
||||||
@@ -831,9 +845,10 @@ begin
|
|||||||
finally
|
finally
|
||||||
FS.Free;
|
FS.Free;
|
||||||
end;
|
end;
|
||||||
// ★Данные пишутся во временный файл и переименовываются поверх занятого
|
// ★Данные пишутся во временный файл, а под целевым именем он появляется
|
||||||
// имени: по имени файла клиент считает запись готовой, и незаконченного
|
// одним вызовом ядра и уже целым: по имени файла клиент считает запись
|
||||||
// содержимого он там видеть не должен. После удачи «.part» не остаётся.
|
// готовой, и незаконченного содержимого он там видеть не должен. После
|
||||||
|
// удачи «.part» рядом не остаётся.
|
||||||
Check('WAV: временный файл убран', PartFiles(Path) = 0,
|
Check('WAV: временный файл убран', PartFiles(Path) = 0,
|
||||||
IntToStr(PartFiles(Path)));
|
IntToStr(PartFiles(Path)));
|
||||||
|
|
||||||
@@ -849,15 +864,15 @@ begin
|
|||||||
TCIRecBudgetUsed = Was, IntToStr(TCIRecBudgetUsed - Was));
|
TCIRecBudgetUsed = Was, IntToStr(TCIRecBudgetUsed - Was));
|
||||||
|
|
||||||
// ★Существующий файл не трогаем: имя приходит из сети, и fmCreate затирал
|
// ★Существующий файл не трогаем: имя приходит из сети, и fmCreate затирал
|
||||||
// бы любой доступный процессу файл. Пишем поверх заведомо другой длиной —
|
// бы любой доступный процессу файл. Публикация идёт без замены
|
||||||
// файл обязан остаться прежним.
|
// (renameat2(RENAME_NOREPLACE) → link), так что занятое имя = отказ. Пишем
|
||||||
|
// поверх заведомо другой длиной — файл обязан остаться прежним.
|
||||||
Tk := MakeTake(Copy(Data, 0, 10), 0);
|
Tk := MakeTake(Copy(Data, 0, 10), 0);
|
||||||
W.Enqueue(Path, Tk, 48000);
|
W.Enqueue(Path, Tk, 48000);
|
||||||
Drained(W, 5000);
|
Drained(W, 5000);
|
||||||
FS := TFileStream.Create(Path, fmOpenRead);
|
FS := TFileStream.Create(Path, fmOpenRead);
|
||||||
try
|
try
|
||||||
// Публикация идёт link(2)/MoveFileW без замены: занятое имя = отказ.
|
Check('WAV: существующий файл не перезаписан',
|
||||||
Check('WAV: существующий файл не перезаписан',
|
|
||||||
FS.Size = 44 + 2000 * 2, IntToStr(FS.Size));
|
FS.Size = 44 + 2000 * 2, IntToStr(FS.Size));
|
||||||
finally
|
finally
|
||||||
FS.Free;
|
FS.Free;
|
||||||
@@ -866,6 +881,21 @@ begin
|
|||||||
DeleteFile(Path);
|
DeleteFile(Path);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// ★Имя «.part» уникально внутри процесса (pid + счётчик), но не между
|
||||||
|
// запусками: файл, оставшийся от прошлой жизни (публиковать было нечем),
|
||||||
|
// плюс повторно выданный системой pid дают EEXIST на создании — и задание
|
||||||
|
// пропадало бы молча. Занимаем ровно то имя, которое писатель возьмёт
|
||||||
|
// следующим, и убеждаемся, что запись всё равно легла.
|
||||||
|
DeleteFile(Path);
|
||||||
|
Squat := NextTempName(TCIRecTempName(Path));
|
||||||
|
FS := TFileStream.Create(Squat, fmCreate);
|
||||||
|
FS.Free;
|
||||||
|
W.Enqueue(Path, MakeTake(Data, 0), 48000);
|
||||||
|
Drained(W, 5000);
|
||||||
|
Check('WAV: занятое имя «.part» не теряет запись', FileExists(Path));
|
||||||
|
DeleteFile(Squat);
|
||||||
|
DeleteFile(Path);
|
||||||
|
|
||||||
// ★Запись не удалась на полпути — по имени не остаётся НИЧЕГО. Прежний код
|
// ★Запись не удалась на полпути — по имени не остаётся НИЧЕГО. Прежний код
|
||||||
// не смотрел на результат записи вовсе (FileWrite и THandleStream.Write при
|
// не смотрел на результат записи вовсе (FileWrite и THandleStream.Write при
|
||||||
// ошибке возвращают 0 и не поднимают исключения), и на полном диске
|
// ошибке возвращают 0 и не поднимают исключения), и на полном диске
|
||||||
@@ -1788,6 +1818,29 @@ begin
|
|||||||
Check('TX слайса: маркеры TX_CHRONO идут', n > 0, IntToStr(n));
|
Check('TX слайса: маркеры TX_CHRONO идут', n > 0, IntToStr(n));
|
||||||
Check('TX слайса: маркер назван номером приёмника (MSHV фильтрует)',
|
Check('TX слайса: маркер назван номером приёмника (MSHV фильтрует)',
|
||||||
(n > 0) and (BadRx = 0), IntToStr(BadRx));
|
(n > 0) and (BadRx = 0), IntToStr(BadRx));
|
||||||
|
|
||||||
|
// ★Команда, которая ничего не сделала, не должна перебивать пару
|
||||||
|
// «клиент + приёмник»: передатчик занят своей же передачей, SyncSetTRX
|
||||||
|
// на неё не отвечает ничем — а если запомнить номер заранее, маркеры
|
||||||
|
// идущей передачи уедут под чужим номером, и клиент замолчит.
|
||||||
|
C.SendText('trx:0,true,tci;');
|
||||||
|
C.WaitText('trx:', 1000);
|
||||||
|
C.Pump(300);
|
||||||
|
n := 0;
|
||||||
|
BadRx := 0;
|
||||||
|
while C.NextFrame(Op, Pay) do
|
||||||
|
if (Op = $02) and (Length(Pay) >= SizeOf(H)) then
|
||||||
|
begin
|
||||||
|
Move(Pay[0], H, SizeOf(H));
|
||||||
|
if H.StreamType = LongWord(Ord(tstTXChrono)) then
|
||||||
|
begin
|
||||||
|
Inc(n);
|
||||||
|
if H.Receiver <> 1 then Inc(BadRx);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
Check('TX слайса: пустая команда не сменила номер приёмника',
|
||||||
|
(n > 0) and (BadRx = 0), IntToStr(BadRx));
|
||||||
|
|
||||||
C.SendText('trx:1,false;');
|
C.SendText('trx:1,false;');
|
||||||
C.WaitText('trx:', 1000);
|
C.WaitText('trx:', 1000);
|
||||||
Ctrl.SetSliceSlotAutoTx(Ctrl.SliceSlotOf(SliceId), False);
|
Ctrl.SetSliceSlotAutoTx(Ctrl.SliceSlotOf(SliceId), False);
|
||||||
|
|||||||
Reference in New Issue
Block a user