mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
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:
+23
-4
@@ -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;
|
||||
|
||||
@@ -1994,6 +1994,7 @@ const
|
||||
PORT = 40099;
|
||||
RATE = 48000;
|
||||
TCI_TX_LEAD_MAX_MS_CHK = 120; // потолок аванса, копия TCI_TX_LEAD_MAX_MS
|
||||
RESTART_CYCLES = 6; // повторных передач в последней фазе
|
||||
var
|
||||
Ctrl: TRadioController;
|
||||
Ad: TTCIAdapter;
|
||||
@@ -2001,6 +2002,7 @@ var
|
||||
Cfg: TTCISettings;
|
||||
C: TRawClient;
|
||||
Markers, Answered, Quantum, LongGaps, MaxGapMs, MinReserve: Integer;
|
||||
N, MinStart, SumStart, StaleRuns: Integer;
|
||||
DbgOwed, DbgFlight: Double;
|
||||
DbgWin, DbgQ, DbgLead, SeedLead: Integer;
|
||||
begin
|
||||
@@ -2146,6 +2148,55 @@ begin
|
||||
Check('пейсинг: защёлки нет, маркеры идут дальше',
|
||||
Markers > 40, IntToStr(Markers));
|
||||
|
||||
// ── Повторные передачи: бухгалтерия не переезжает в следующую ──
|
||||
// ★Регресс с железа. Штатный конец посылки (trx:N,false — так её и кончает
|
||||
// MSHV каждый цикл FT8) обнулял FTxClient, но НЕ FTxRunning, а сбрасывать
|
||||
// флаг умел только тик планировщика — и только пока видел ЖИВОГО FTxClient
|
||||
// с уже снятым TCIMicActive. Окно этой гонки — хвост SetMOX, пара
|
||||
// миллисекунд против шага тика в полкванта, так что промах выпадал через
|
||||
// раз. Промах означал, что СЛЕДУЮЩАЯ передача идёт мимо TxResetAccounting:
|
||||
// долг сразу у потолка, в полёте чужие кредиты прошлой посылки, выдача
|
||||
// маркеров заблокирована до срабатывания сторожа (выдержка до 250 мс). В
|
||||
// эфире это пересохшая очередь DUC с первых миллисекунд — голая несущая
|
||||
// гетеродина DUC рядом с сигналом.
|
||||
// Одиночный промах случаен, поэтому гоняем пачкой: до правки хоть один из
|
||||
// шести циклов ловится практически всегда.
|
||||
MinStart := MaxInt;
|
||||
SumStart := 0;
|
||||
StaleRuns := 0;
|
||||
for N := 1 to RESTART_CYCLES do
|
||||
begin
|
||||
C.SendText('trx:0,false;');
|
||||
C.WaitText('trx:0,false', 1500);
|
||||
// ★Инвариант проверяем сразу по подтверждению команды, не дожидаясь тика:
|
||||
// флаг обязан опускаться под тем же FTxLock, что и FTxClient, то есть до
|
||||
// ответа клиенту. В этом и смысл правки.
|
||||
if Ad.TxDbgState(DbgOwed, DbgFlight, DbgWin, DbgQ, DbgLead) then
|
||||
Inc(StaleRuns);
|
||||
C.SendText('trx:0,true,tci;');
|
||||
// Отвечаем СРАЗУ, не дожидаясь текстового подтверждения: WaitText спит по
|
||||
// 50 мс и выбрасывает бинарные кадры, а брошенный без ответа маркер — это
|
||||
// зависший кредит, который сам придушит выдачу и смажет измерение старта.
|
||||
ServeChrono(C, 200, 0, Markers, Answered, Quantum, LongGaps, MaxGapMs,
|
||||
MinReserve);
|
||||
if Markers < MinStart then MinStart := Markers;
|
||||
Inc(SumStart, Markers);
|
||||
// Тело передачи: к её концу часть кредитов остаётся в полёте — именно их
|
||||
// и наследовала следующая посылка.
|
||||
ServeChrono(C, 150, 0, Markers, Answered, Quantum, LongGaps, MaxGapMs,
|
||||
MinReserve);
|
||||
end;
|
||||
WriteLn(Format(' .. повторы: %d передач подряд, маркеров в первые 200 мс ' +
|
||||
'минимум %d, в среднем %d; флаг протухал %d раз',
|
||||
[RESTART_CYCLES, MinStart, SumStart div RESTART_CYCLES,
|
||||
StaleRuns]));
|
||||
Check('повторы: конец передачи опускает флаг идущей передачи',
|
||||
StaleRuns = 0, Format('%d из %d', [StaleRuns, RESTART_CYCLES]));
|
||||
// 200 мс / 10.67 мс = около 18 маркеров в здоровом старте; отравленный
|
||||
// старт не даёт ни одного, пока не сработает сторож.
|
||||
Check('повторы: маркеры идут с первых миллисекунд КАЖДОЙ передачи',
|
||||
MinStart >= 8, Format('минимум %d маркеров за 200 мс', [MinStart]));
|
||||
|
||||
C.SendText('trx:0,false;');
|
||||
C.Pump(300);
|
||||
finally
|
||||
|
||||
Reference in New Issue
Block a user