fix(tci): бухгалтерия пейсинга переезжала из прошлой передачи в следующую

Посреди передачи из MSHV рядом с сигналом вставала вторая несущая — и не
изредка, а с первых миллисекунд посылки. Это голая несущая гетеродина DUC:
очередь пересыхает, модуляции нет.

Цепочка. Конец посылки клиент делает командой trx:N,false. SetMOX(False)
снимает TCIMicActive внутри Invoke, а FTxClient обнуляется ПОСЛЕ возврата из
него, в HandleTrx. Опустить FTxRunning умел только тик планировщика — и только
пока видел живого FTxClient с уже снятым TCIMicActive. Окно = хвост SetMOX,
пара миллисекунд против шага тика в полкванта: тик попадал в него через раз.

Промах означал, что следующая передача идёт МИМО TxResetAccounting и наследует
FTxOwed, FTxInFlight, FTxSatSinceUs и оценку задержки. Долг сразу у потолка,
в полёте чужие кредиты прошлой посылки, условие выдачи не выполняется — маркеры
не идут вовсе, пока не сработает сторож (выдержка до 250 мс). Нулевой pre-roll
кончается раньше, очередь DUC сохнет, радио отдаёт несущую.

Лечение — инвариант «нет клиента, нет и идущей передачи», как в ClearTxClient
(он его соблюдал, а HandleTrx нет):
* FTxRunning := False рядом с FTxClient := Client — первый тик передачи всегда
  начинает с TxResetAccounting, каким бы ни был исход гонки;
* то же в обоих местах, где источник снимается;
* то же в выходе PushTxChrono по C = nil — страховка от будущих путей.

Аванс (FTxLeadKeep, FTxGapUs) не трогаем: он намеренно переживает передачу,
на нём стоит pre-roll следующего PTT.

Стенд: новая фаза «повторы» в TestTxPacing — шесть циклов PTT на одном клиенте,
проверка флага сразу по подтверждению trx:0,false и счёт маркеров в первые
200 мс каждой передачи. Негативный контроль на старом коде: флаг протухал 6/6,
маркеров 4 вместо 22. Стенд 262/262, test/cat 50/50, GUI и демон собираются.

★Отдельной тестовой процедурой это сделать нельзя: третий TRadioController в
процессе даёт AV в FreeEngines (состояние WDSP), стенд падает до проверок.
Фаза живёт внутри TestTxPacing и стартует после потерь ответов — то есть с
заведомо отравленной бухгалтерией.

★Почему дефект вылез только сейчас: TraceDump('tx') из снятых зондов стоял в
конце SetMOX(False) и писал до 16 тысяч строк — десятки миллисекунд ровно в
этом окне, и тик гарантированно успевал. Прогоны шли с EWSDR_TXTRACE=1, так
что инструмент собой же и прикрывал гонку.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
This commit is contained in:
2026-08-24 18:45:55 +03:00
co-authored by Claude Opus 5
parent 166fc65fc8
commit fa38fa7f75
2 changed files with 74 additions and 4 deletions
+23 -4
View File
@@ -2185,6 +2185,17 @@ begin
begin
FTxClient := Client;
FTxRx := Rx;
// ★Новая передача — новая бухгалтерия, и опустить флаг обязаны
// ЗДЕСЬ. Сбрасывает его тик, но только пока видит ЖИВОГО FTxClient
// и уже снятый TCIMicActive (см. PushTxChrono) — а это окно длиной
// в хвост SetMOX, пара миллисекунд против шага тика в полкванта.
// Тик в него попадал через раз, и промах означал, что следующая
// передача идёт МИМО TxResetAccounting: с долгом у потолка и
// чужими кредитами в полёте, то есть с заблокированной выдачей
// маркеров на всю выдержку сторожа. В эфире это голая несущая
// гетеродина DUC поверх пересохшей очереди с первых же
// миллисекунд посылки.
FTxRunning := False;
// ★Будим планировщик немедленно. Иначе первый маркер уходил на
// 10-20 мс позже фронта PTT (дремотный шаг), и ровно этих
// миллисекунд не хватало нулевому pre-roll, чтобы дожить до первого
@@ -2194,15 +2205,20 @@ begin
// Эфир поднял он же, но модуляцию просит НЕ из TCI — снимаем свою.
else if Started and (not Tune) and (FTxClient = Client) then
begin
FTxClient := nil;
FTxRx := 0;
FTxClient := nil;
FTxRx := 0;
FTxRunning := False;
end;
end
// Сам просит выключить — источник ему больше не нужен в любом случае.
else if (not Tune) and (FTxClient = Client) then
begin
FTxClient := nil;
FTxRx := 0;
FTxClient := nil;
FTxRx := 0;
// ★Тот же инвариант, что и в ClearTxClient: нет клиента — нет и
// идущей передачи. Раньше эти две строки расходились, и штатный
// конец посылки (trx:N,false у MSHV) оставлял FTxRunning поднятым.
FTxRunning := False;
end;
finally FTxLock.Leave; end;
end;
@@ -3820,6 +3836,9 @@ begin
try
C := FTxClient;
Rx := FTxRx;
// Хозяина нет — считать нечего и не про кого. Флаг снимаем под тем же
// локом: он живёт ровно столько, сколько живёт FTxClient.
if C = nil then FTxRunning := False;
finally
FTxLock.Leave;
end;