From fa38fa7f7572879569aadc72985a255515abd064 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 24 Aug 2026 18:45:55 +0300 Subject: [PATCH] =?UTF-8?q?fix(tci):=20=D0=B1=D1=83=D1=85=D0=B3=D0=B0?= =?UTF-8?q?=D0=BB=D1=82=D0=B5=D1=80=D0=B8=D1=8F=20=D0=BF=D0=B5=D0=B9=D1=81?= =?UTF-8?q?=D0=B8=D0=BD=D0=B3=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B5=D0=B7?= =?UTF-8?q?=D0=B6=D0=B0=D0=BB=D0=B0=20=D0=B8=D0=B7=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=88=D0=BB=D0=BE=D0=B9=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0?= =?UTF-8?q?=D1=87=D0=B8=20=D0=B2=20=D1=81=D0=BB=D0=B5=D0=B4=D1=83=D1=8E?= =?UTF-8?q?=D1=89=D1=83=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Посреди передачи из 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 Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV --- TCIAdapter.pas | 27 +++++++++++++++++++---- test/tci/tcitest.pas | 51 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/TCIAdapter.pas b/TCIAdapter.pas index 2302b8b..891a347 100644 --- a/TCIAdapter.pas +++ b/TCIAdapter.pas @@ -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; diff --git a/test/tci/tcitest.pas b/test/tci/tcitest.pas index 415ed03..44aaf42 100644 --- a/test/tci/tcitest.pas +++ b/test/tci/tcitest.pas @@ -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