feat(tx): тихий детектор голодания очереди TX

Всплеск, рождённый нашим трактом, невозможен без одного события: очередь DUC
простаивает дольше подушки отправителя (DUC_FIFO_THROTTLE = 2000 отсчётов при
192 кГц = 10.42 мс). Съём 2026-08-21 это показал прямо: шесть всплесков — шесть
простоев 10.3-23.5 мс, а паузы 8 мс и короче не дали ни одного. Значит счётчик
таких простоев — полный детектор «наш/не наш», и оператору больше не нужно
называть интервал на глаз.

Новый юнит TxHealth.pas. Включён всегда: переменной окружения нет намеренно —
прибор, который надо не забыть включить, к моменту дефекта выключен.

* поток отправителя DUC мерит каждый простой очереди на передаче — одно чтение
  монотонных часов на ПЕРЕХОД, ни файлов, ни локов (обе ветки, win и unix);
* планировщик TCI публикует рядом состояние пейсинга (аванс, долг, в полёте,
  квант, шаг, период пачек клиента) — простыми записями, без лока: числа
  диагностические, а лок в потоке отправителя DUC недопустим для тайминга;
* сетевой поток считает бит HPS_FIFO_EMPTY из HP-статуса радио — поле ГЛУБИНЫ
  FIFO на этой прошивке статично (256) и веры ему нет, а бит мы не читали вовсе;
* ★журнал пишет ТОЛЬКО потребитель — таймер UI (100 мс) и цикл демона через
  TxHealthService. В тракте передачи диска нет по построению.

Последнее — правило, оплаченное регрессом: прошлый прибор (TxTrace) сбрасывал
дамп синхронно в хвосте SetMOX, и эти десятки миллисекунд закрывали гонку в
пейсинге TCI (лечение — fa38fa7). Инструмент чинил дефект собой.

Журнал ~/.config/ewsdr/txhealth.log, строка на передачу: «чисто» либо «ОПАСНО
(первое на N мс)» плюс подробности с контекстом пейсинга. В статусной строке
UI — «·сухо N» за сеанс, чтобы не помнить, какая посылка была плохой.

Стенд: секция G в test/tci (порог подушки, сводка, момент от фронта PTT,
контекст рядом с событием, молчание вне передачи) — 276/276. Диск стенд не
трогает намеренно: берёт сводки через TxHealthTake/TxHealthDetail, мимо
TxHealthService, иначе писал бы в настоящий журнал пользователя.

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 19:23:13 +03:00
co-authored by Claude Opus 5
parent fa38fa7f75
commit b173b7f542
8 changed files with 559 additions and 10 deletions
+23 -1
View File
@@ -51,7 +51,7 @@ uses
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder,
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, CWMorse, CWKeyer,
CWDecoder;
CWDecoder, TxHealth;
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
@@ -1864,6 +1864,11 @@ begin
FHPpPLL := (Status.StatusBits and HPS_PLL_LOCKED) <> 0;
FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0;
FHPpOverload := Status.ADCOverload;
// ★Жалоба самого радио: бит «DUC FIFO пуст». Поле ГЛУБИНЫ FIFO (байты 35-36)
// на этой прошивке статично и веры ему нет, а бит мы не читали вовсе —
// если прошивка его заполняет, это независимый свидетель голодания.
if FTransmitting and ((Status.StatusBits and HPS_FIFO_EMPTY) <> 0) then
TxHealthFifoEmpty;
// Состояние ключа/манипулятора в разъёме трансивера (сетевой поток).
// Касание обрывает программную передачу — как в Thetis/pihpsdr («key hit»
// останавливает CAT CW), иначе оператор не может вклиниться. ★Обрываем по
@@ -6292,6 +6297,7 @@ end;
procedure TRadioController.SetMOX(On_: Boolean);
var
WasTransmitting: Boolean;
TxhSrc: Integer;
begin
// DMR support is receive-only. This guard also covers CAT, web and hardware
// PTT. FM RAW is a flat external-modem transmit path.
@@ -6426,6 +6432,19 @@ begin
if FTransmitting and FDisplayDuplex then
FDSPEngine.SetDisplaySourceTX(False);
end;
// Детектор голодания очереди TX: фронт «в эфир» открывает передачу для
// счётчиков. Стоит ПОСЛЕ блока движка — источник модуляции решается там.
// Ввода-вывода тут нет и быть не может: журнал пишет потребитель (таймер UI /
// цикл демона), см. TxHealth.pas.
if FTransmitting and (not WasTransmitting) then
begin
if FTCIMicActive then TxhSrc := txhSrcTCI
else if FWebMicActive then TxhSrc := txhSrcWeb
else if Assigned(FDSPEngine) and (FDSPEngine.TXMicSource = txmsSoundCard) then
TxhSrc := txhSrcCard
else TxhSrc := txhSrcRadio;
TxHealthPtt(True, TxhSrc);
end;
// Сетевая PTT-последовательность TX-off: сначала снимаем PTT, затем реле RX.
if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
begin
@@ -6443,6 +6462,9 @@ begin
// (оператор держит T/R руками) — вооружение обязано пойти за этой кнопкой.
SyncCWKeyer;
Changed(rfTransmitting);
// Передача кончилась — складываем сводку в память. Диск не трогаем (★прошлый
// прибор писал дамп ровно здесь и тем самым прикрывал гонку в пейсинге).
if WasTransmitting and (not FTransmitting) then TxHealthPtt(False, 0);
end;
procedure TRadioController.SetTune(On_: Boolean);