Всплеск, рождённый нашим трактом, невозможен без одного события: очередь 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
17 KiB
Разбор всплесков на передаче по трафику радио (openHPSDR, протокол 2)
Всплеск на передаче в этом тракте может родиться в трёх разных местах, и глазами их не отличить. Дамп трафика отличает, потому что в проводе лежит ровно то, что мы отдали радио, и ровно то, что радио вернуло:
| где родился | что видно в дампе |
|---|---|
| наш DSP / очередь TX | разрыв внутри DUC I&Q: скачок на стыке пакетов, клиппинг, дыра по времени |
| сеть / провод | потеря seq DUC I&Q (наружу) или DDC I&Q (внутрь), дубли, переупорядочение |
| радио / прошивка | DUC I&Q ушёл ровным, а радио жалуется: FIFO overflow, пустой DUC FIFO |
| приёмный тракт ПК | на проводе всё цело, но растут RcvbufErrors/drops — мусор на водопаде, а не в эфире |
Последняя строка важнее, чем кажется: всплеск, увиденный на своём водопаде, может вообще не уйти в эфир — это может быть дыра в приёмном потоке DDC. Инструмент это разделяет.
1. Съём
sudo test/hpsdr/capture.sh # bridge0, радио 172.16.2.200
sudo test/hpsdr/capture.sh -i eth0 -h 172.16.2.99 -d /var/tmp/cap
Пишется кольцо -W 24 -C 150 (24 файла по 150 МБ ≈ 10-15 мин полного дуплекса)
плюс *.drops — счётчики потерь ядра раз в секунду.
Явление редкое, поэтому работаем так: запустили съём, работаем в эфире как обычно. Увидели всплеск — засекли время по часам и дали ещё секунд десять, потом Ctrl-C. Кольцо гарантирует, что последние минуты на диске, а засечённое время сразу показывает, в какую строку отчёта смотреть.
Место: полный дуплекс на 192 кГц TX + пара DDC ≈ 3-6 МБ/с. Кольцо не растёт.
2. Разбор
test/hpsdr/pcap_tx_scan.py /tmp/hpsdrcap/hpsdr-*.pcap \
--watch-ddc 3 --drops /tmp/hpsdrcap/hpsdr-*.drops
--watch-ddc N — главный ключ, если вы смотрите свой сигнал на конкретном
приёмнике (DDC3 = UDP-порт 1038). Тогда скан ищет всплески прямо в приёмном
потоке и сопоставляет каждый с тем, что в этот момент ушло в эфир. Без ключа
всплески ищутся на всех DDC.
Прочие ключи: --tx-only (события только внутри передачи), --radio IP (если
автоопределение промахнулось), --corr СЕК (окно сопоставления, по умолчанию
0.05), --max N, --env env.csv (профиль огибающей по пакетам — можно
построить график и увидеть форму всплеска).
3. Как читать отчёт
=== передачи === — по каждому нажатию PTT: сколько DUC-пакетов реально
ушло против расчётных длительность / 1.25 мс. Минус десятки пакетов = очередь
TX голодала, в эфире дыры. Тут же диапазон глубины DUC FIFO радио: здоровая
передача держит его далеко от нуля.
=== сопоставление === — ради этого всё и делалось. По каждому всплеску,
найденному в приёмном потоке, печатается всё, что случилось в окне ±50 мс, со
смещением в миллисекундах, и вердикт. Смещение важно: свой сигнал возвращается
в приёмник с задержкой тракта (ЦАП, эфир, конвейер DDC), поэтому причина обязана
стоять с минусом — раньше всплеска. Вердикты: НАШ ТРАКТ (в DUC I&Q рядом
BREAK/CLIP — сигнал ушёл в провод уже испорченным), АРТЕФАКТ ПРИЁМА (дыра
в самом приёмном потоке или SOCK-DROP — в эфир всплеск не уходил), РАДИО,
ПРОВОД, КОМАНДА НА ХОДУ, ЖЕЛЕЗО (рядом чисто в обе стороны).
=== конфигурация DDC === — из перехваченного DDC Specific: источник
каждого включённого DDC (ADC0 = эфир, DAC = петля PureSignal) и его частота
дискретизации. Источник решает, что вообще означает увиденный всплеск: с ADC —
это сигнал в антенне, с DAC — только то, что ушло в модулятор.
События, по убыванию доказательности:
RX-SPIKE— выброс над фоном в приёмном потоке. Ищется только на передаче: там свой сигнал держит ровный уровень, и всё, что торчит над ним, — событие. На приёме эфир скачет сам, детектор был бы бессмысленным. Первые 250 мс после фронта PTT пропускаются: уровень там меняется законно.BREAK— последний отсчёт пакета N и первый отсчёт N+1 разошлись больше чем на 45% огибающей. На 192 ksps соседние отсчёты речевого сигнала так не прыгают: это разрыв фазы/амплитуды, то есть щелчок или всплеск шириной во весь фильтр. Родился до провода, значит у нас — в WDSP, в сборке пакетов или в очереди. (Ровно эта подпись однажды уже ловилась:bfo=0вOpenChannel(TXA).) Ищется только в исходящем DUC: на приёме шум эфира прыгает от отсчёта к отсчёту сам по себе, а разрыв от потерянного пакета и так виден поseq.CLIP— упор в полную шкалу DAC, печатается по фронту с длительностью.TX-STARVE/FIFO-OVF— жалоба самого радио: DUC FIFO пуст на передаче либо переполнение. При пустом FIFO радио передаёт что попало.SOCK-DROP— на проводе пакеты были, ядро их выбросило. Тогда всплеск на экране — артефакт приёма, в эфир он не уходил.HOLE— пауза между пакетами больше 2.5 периодов потока. Для DUC норма 1.25 мс, для DDC период выучивается по медиане первых 200 интервалов.SEQ-LOSS/SEQ-DUP— потери и дубли в проводе, отдельно наружу и внутрь.HP-CHG— High Priority сменился ПОСРЕДИ передачи, с расшифровкой поля (частота DDC/DUC, drive, Alex, атт., OC). Смена частоты или drive на ходу — это клик по построению.DUCSPEC/DDCSPEC— Specific-пакеты, ушедшие на передаче.
Пусто во всех разделах при засечённом всплеске — значит трактовка меняется: цифровой поток чист от нас до радио и обратно, ищем дальше по железу (PA, ALC, питание, реле), а не в софте.
Файлы
capture.sh— кольцевой съём tcpdump + лог потерь ядра.pcap_tx_scan.py— разбор pcap, чистый Python без зависимостей (~40 МБ/с).
Тихий детектор голодания очереди TX (включён всегда)
Всплеск, рождённый НАШИМ трактом, невозможен без одного события: очередь DUC
простаивает дольше подушки отправителя (DUC_FIFO_THROTTLE = 2000 отсчётов при
192 кГц = 10.42 мс). Это доказано съёмом 2026-08-21: шесть всплесков — шесть
простоев 10.3–23.5 мс, паузы 8 мс и короче не дали ни одного. Поэтому счётчик
таких простоев — полный детектор, и он теперь живёт в самой программе
(TxHealth.pas), без переменных окружения: прибор, который надо не забыть
включить, к моменту дефекта выключен.
Что делает:
- поток отправителя DUC мерит каждый простой очереди на передаче — одно чтение монотонных часов на переход, ни файлов, ни локов;
- планировщик TCI публикует рядом состояние пейсинга (аванс, долг, в полёте, квант, шаг, период пачек клиента) — простыми записями;
- сетевой поток считает биты «DUC FIFO пуст» из HP-статуса радио (поле ГЛУБИНЫ на этой прошивке статично и веры ему нет, а бит раньше не читался вовсе);
- журнал пишет потребитель — таймер UI (100 мс) или цикл демона, никогда не тракт передачи.
★Последнее — правило, оплаченное регрессом: прошлый прибор (TxTrace) сбрасывал
дамп синхронно в хвосте SetMOX, и эти десятки миллисекунд закрывали гонку в
пейсинге TCI. С зондами всплесков не было, без них они возвращались —
инструмент чинил дефект собой (лечение: коммит fa38fa7).
Журнал: ~/.config/ewsdr/txhealth.log, по строке на передачу.
2026-08-24 19:08:03 ОПАСНО (первое на 100 мс) передача 12.4 с, источник TCI: осушений 17, опасных 1, самое длинное 18.0 мс, FIFO-EMPTY 0
опасный #1: на 100 мс от PTT, 18.0 мс, очередь 0, аванс 3584, долг 2200, в полёте 1024, квант 512, шаг 5333 мкс, пачка клиента 44.1 мс
Как читать:
чисто— за эту передачу очередь ни разу не пересыхала дольше подушки. Значит увиденный всплеск родился НЕ в нашем тракте: смотреть приёмный тракт (дыра в DDC — на экране есть, в эфире нет), PA, реле, спуры.ОПАСНО— простой был, и подробная строка называет виновника по контексту: пачка клиента длиннее аванса (пачка клиента>аванс/48 мс) — зернистость MSHV;долгу потолка при маломв полёте— планировщик не успевает;шагзаметно больше половины кванта — проспал поток планировщика.- Строка статуса в UI показывает
·сухо N— сколько опасных простоев набралось за сеанс, чтобы не приходилось помнить, какая посылка была плохой.
Правило разбора то же, что и с pcap: дальше сопоставлять. Детектор говорит
«очередь сохла в такой-то момент», pcap_tx_scan.py по тому же куску времени
скажет, ушёл ли всплеск в эфир или это артефакт приёма.
Приборы внутри процесса: кольцевая трассировка TX-тракта
★Сняты из кода 2026-08-24 — пейсинг TX проверен на железе, всплесков нет, и
инструмент больше не нужен в горячем пути. Последняя редакция зондов лежит в
коммите d209697; вернуть их целиком:
git show d209697 -- TxTrace.pas | git apply # сам юнит
git show <коммит снятия> | git apply -R # точки съёма и TraceInit
Скрипт разбора (txtrace_scan.py) оставлен: формат дампа не менялся, и при
возврате зондов он работает как прежде. Ниже — описание прибора на случай
возврата.
Дамп на проводе видит ПАУЗУ в потоке DUC, но не видит, кто её сделал. Для этого
в код вписывалось кольцо трассировки (TxTrace.pas), выключенное по умолчанию.
Включение — переменной окружения, кольцо сбрасывается на диск в конце КАЖДОЙ передачи (после снятия PTT, чтобы файл не задержал выход из эфира):
EWSDR_TXTRACE=1 ./bin/x86_64-linux/ewsdr
# файлы: ~/.config/ewsdr/txtrace-<дата>-tx.txt
Разбор: ./txtrace_scan.py ~/.config/ewsdr/txtrace-*-tx.txt
Что пишется (только выбросы там, где событий тысячи в секунду):
| событие | A | B | C |
|---|---|---|---|
PTT |
1 старт / 0 конец | источник модуляции: 0 радио, 1 звуковая карта, 2 внешняя подача (web/TCI) | |
RECV-GAP |
пауза между возвратами recvfrom >2 мс |
порт | |
HANDLER |
длительность обработчика >2 мс | порт | длина пакета |
HP-SYNC |
длительность Invoke(ApplyHPStatus) = TThread.Synchronize >2 мс |
||
TX-TICK |
зазор между тиками TX-потока | уровень mic-ринга НА ВХОДЕ тика | блоков сделано |
TX-PHASE |
fexchange0 |
кормление TX-анализатора (стоит ПЕРЕД отдачей IQ) | отдача IQ в очередь DUC |
CHRONO |
сколько отсчётов просим у клиента TCI | остаток долга | пауза от прошлого маркера |
TX-AUDIO |
пауза от прошлого блока от клиента | length из шапки клиента |
отсчётов после интерполяции |
PULL-MIC |
запрошено у звуковой карты | реально получено | пауза от прошлой тяги |
DUC-DRY |
виртуальный уровень FIFO радио | глубина очереди | |
DUC-RUN |
длительность простоя очереди | уровень FIFO на выходе | глубина очереди |
Ключевые чтения:
DUC-RUNдлиннее 10.42 мс (подушкаDUC_FIFO_THROTTLE) = кандидат на всплеск; скрипт печатает историю событий перед каждым таким простоем;TX-TICKсBменьше 512 = продюсер не успел, тракт тут ни при чём;TX-AUDIOс периодом около 40-60 мс = зернистость запроса TCI (тик 20 мс против блока 2048 отсчётов = 42.7 мс), а не сбой сети;- пара (
B,C) уTX-AUDIOпроверяет учёт каналов: при стереоlengthобязан быть вдвое больше моно-отсчётов до интерполяции.