# Разбор всплесков на передаче по трафику радио (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` обязан быть вдвое больше моно-отсчётов до интерполяции.