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