Files
ewsdr/test/hpsdr/README.md
T
ew8bakandClaude Opus 5 b173b7f542 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
2026-08-24 19:23:13 +03:00

206 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Разбор всплесков на передаче по трафику радио (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`
обязан быть вдвое больше моно-отсчётов до интерполяции.