Improve BLE call batching, startup buffering and diagnostics

This commit is contained in:
evgeny committed 2026-10-30 16:44:52 +01:00
1 parent 0afccfb624
commit 049c8ef155
44 files changed
+1406 -245

No files matched your search

+7 -1
View File
@@ -1,6 +1,12 @@
BLE GATT → ETCP (chatgui desktop + Android)
Текущее состояние (29.10.2026):
Обновление30.10.2026: Android предпочитает PHY1M. Common BLE ограничивает число
целых DATA до ACK последнего фрагмента (config.tx_window); очередь tx_limit
и защитный предел12500байт учитываются дополнительно. Формат DATA/ACK прежний.
Выбор окна и актуальный контракт — doc/ble_transport.txt. Новые аппаратные
измерения выполнены отдельным стендом драйвера без ETCP.
Историческое состояние (29.10.2026):
Write Without Response/Notify, HIGH и PHY2M включены. BLE version3 добавляет
16-bit ID каждого DATA GATT-value, накопительный ACK по отдельной характеристике
не чаще15ms и окно12500байт на направление (фиксированные2Мбит/с ×50ms).
+34 -10
View File
@@ -61,13 +61,17 @@ RX при заполнении отбрасывает целый новый ка
если он присутствует. Это не Ethernet payload MTU 1500 (которому нужно >=1514).
На link не больше одной незавершённой GATT-передачи. Она хранит свой fragment
до завершения ОС. Очередь ядра/Qt не используется как неограниченный TX-буфер.
DATA дополнительно ограничен неподтверждёнными GATT-values: фиксированные2Мбит/с
×50ms/8 =12500байт на направление, включая flow/framing headers. Локальный sent
не возвращает этот кредит: его освобождает накопительный ACK удалённого драйвера.
DATA дополнительно ограничен config.tx_window: 0 означает BLE_TX_WINDOW_FRAMES=3,
явное значение1..64. В окно входят все принятые tx_put кадры: очередь, текущая
фрагментация и ожидание удалённого ACK последнего фрагмента. Локальный sent
и ACK середины кадра не освобождают слот. Это учёт существующих flow ACK,
без новой wire-команды и без гарантии принятия кадра приложением.
Независимый защитный предел GATT-values —12500байт на направление, включая
flow/framing headers; байтовый кредит освобождается накопительным удалённым ACK.
При заполнении ble_tx_ready=false; ACK будит writable после появления места.
Текущий кадр может остановиться посреди фрагментации, INVITE и ACK продолжают работать.
Предел12500 — расчётный размер по фиксированной скорости, не гарантия фактических50ms
при медленном радио. Сверх окна остаётся только ограниченная собственная TX-очередь.
Предел12500 не задаёт скорость или время доставки. Собственная TX-очередь входит
в окно целых кадров; предел config.tx_limit дополнительно ограничивает её размер.
Переподключение не переносит старые кадры и не повторяет неоднозначные отправки.
Wire protocol
@@ -152,7 +156,7 @@ Value/Confirm без адреса клиента для серверной пе
Выходящий клиент: DATA/ACK WriteValue(type=command), INVITE WriteValue(type=request),
StartNotify для TX, INVITE и ACK; открытие после всех трёх подписок.
Публичный Device1 D-Bus API не задаёт per-link PHY/connection interval: согласованием
управляют ОС и peer; Android запрашивает 2M также в серверной роли.
управляют ОС и peer; Android запрашивает 1M также в серверной роли.
Не вызывается глобальный Device1.Disconnect: ACL может обслуживать другие сервисы.
Проверено с BlueZ 5.85; требуется серверный AcquireNotify с indication completion.
@@ -400,15 +404,35 @@ Instrumentation временно включает echo, возвращает с
Профиль скорости Android
------------------------
После подключения client просит CONNECTION_PRIORITY_HIGH и ATT MTU 517.
Обе роли запрашивают PHY_LE_2M для TX/RX при поддержке локального контроллера.
Обе роли запрашивают PHY_LE_1M для TX/RX: аппаратные сравнения A52/Linux показали
падение полезной скорости на2M при занятой очереди контроллера.
Запрос не гарантирует смену PHY: onPhyUpdate/onPhyRead логируют фактические TX/RX.
При отсутствии поддержки остаётся согласованный PHY. S2/S8 — параметры только
LE Coded PHY (дальность/скорость), с выбранным 2M применяется NO_PREFERRED.
S2/S8 — параметры только LE Coded PHY; для1M применяется NO_PREFERRED.
У Android BluetoothGattServer нет public requestConnectionPriority; входящий link
использует дополнительный BluetoothGatt handle того же ACL только для HIGH.
Запрос/результат логируются, handle закрывается при teardown; нового ACL для настройки нет.
Сам UUID/advertising остаётся legacy
1M для обнаружения; PHY 2M относится к установленному соединению.
1M для обнаружения; предпочтение PHY отдельно задаётся установленному соединению.
Подбор окна 2026-10-30
---------------------
Отдельный стенд без ETCP: A52 Android-central ↔ Linux/BlueZ, PHY1M, MTU517,
interval15ms, DLE251/time2120us. Каждая проба:5s туда,5s обратно,5s дуплекс,
побайтовая проверка1500B кадров. Окно квитанций стенда64, ограничивает драйвер.
Скорости полезной нагрузки, кбит/с: окно1 —178/222, дуплекс241; окно2 (два
прохода) —374/399..406, дуплекс341..343; окно3 (два прохода) —378..385/413..416,
дуплекс390..395; окно4 —385/418, дуплекс418. Max RTT до квитанции соответственно
137/155/196/245ms. Окно3 выбрано как минимальное близкое к насыщению канала
с RTT<=200ms в этих пробах; окно2 снижает дуплексную скорость примерно на13%.
Окно драйвера освобождается flow ACK, поэтому не равно окну квитанций стенда.
С активной BLE-мышью DLE time падал до340us, наблюдались RTT>200ms даже при
малом окне. Фиксированное окно не гарантирует200ms при произвольной нагрузке
радио. Эти короткие замеры не являются долговременным SLA и не включают ETCP.
Счётчики frames/window_frames/peak_frames и frames_released показывают
ограничение и освобождение кадров; PHY callbacks показывают фактический PHY.
Контроль после финальной сборки со штатными defaults:381/423кбит/с, дуплекс388,
maxRTT177.018ms, обе стороны PASS. При Linux-central окно3 также прошло:
378/420кбит/с, дуплекс381, maxRTT181.328ms. Все три BLE unit/mock-теста PASS.
Проверка 2026-10-29
------------------
+126
View File
@@ -0,0 +1,126 @@
Адаптивная упаковка Opus в звонке
================================
Сервис CALL_AUDIO использует одну Router-кодограмму для 1..10 Opus-кадров.
Кодек и длительность кадра (20мс) не меняются. Новый формат должен быть
на обеих сторонах; старый одиночный CALL_MEDIA не поддерживается.
Wire, packed, порядок байтов хоста как у остального CALL:
Общий header, 27 байт:
subcmd:u8, call_id:u64, track_id:u8, capture_epoch:u64,
codec_delay_samples:u32, sent_ms:u32, count:u8.
Далее count раз:
seq:u16, capture_pts_us:i64, length:u16, opus[length].
Накладные расходы: 27 + 12*N байт CALL на N кадров, плюс один Router.
count=1..10, весь CALL_MEDIA <=1024 байт. Пачка может быть меньше target из-за
размера, срока отправки или смены capture epoch/codec delay. PTS/seq каждого
кадра сохраняются; PTS может быть INT64_MIN. Парсер проверяет всю кодограмму
до выдачи первого кадра, отвергает хвостовые байты и незавершённые записи.
Headless callback по-прежнему получает отдельные Opus-кадры.
Пример для Opus по 37 байт, прямой Router с SIGN|ENCRYPT (+163 байта):
N кадров CALL payload Router wire байт Router на кадр
1 76 239 239
2 125 288 144
3 174 337 112,33
5 272 435 87
10 517 680 68
Обрамление ETCP/BLE и ACK не включены. Normalizer может объединять/разбивать
кодограммы. Старый расход: 236 байт Router на каждый такой Opus-кадр;
историческое разложение — call_audio_packet_overhead.txt.
Источник обратной связи
-----------------------
etcp_router_tx_stats(group_id, peer_node_id, ETCP_RT_ID_CALL_AUDIO).
Это конкретный логический аудиоканал, не общая сумма очередей ядра.
Интерфейс уже используется видеотреком звонка; stream_publisher использует
другой, общий снимок ETCP и для данного регулятора не подходит.
Router сохраняет исходное монотонное enqueued_us в send_q и переносит его
в inflight. Ретрансмит не меняет эту метку. При первом подтверждении:
sample = ACK time - enqueued_us;
delivery_rtt_us = EWMA sample с весом 1/8 (первый sample без сглаживания).
Снимок также отдаёт queued_age_us/inflight_age_us головы от исходного enqueue
и last_ack_us. Возраст пустой очереди равен нулю. При reset история очищается.
Прежние rtt_us/baseline_rtt_us и алгоритм видеорегулятора не меняются.
Полный delivery_rtt_us включает исходную очередь Router, нижние очереди,
путь до приёмника и возврат ACK. Базовый RTT НЕ вычитается. Это не время
доставки в динамик и не измерение односторонней задержки.
Регулятор
---------
Снимок каждые50мс в uasync; управление целиком в call_session.
D = max(delivery_rtt_us, queued_age_us, inflight_age_us).
Собственное намеренное накопление пачки в D не добавляется.
Начальное target=4, deadline60мс от первого кадра: это время сбора ещё трёх
кадров по20мс. При смене local/peer Router epoch или исчезновении канала
target/история сбрасываются. Первый пригодный ACK сразу переводит deadline
на измеренную задержку, даже до заполнения окна статистики500мс; на быстром
канале ожидание сразу выключается. Для изменения target нужно полное окно.
До первого ACK сохраняется стартовое ожидание60мс; при no_route, receive_busy
и minRTT probe ожидание и рост выключены.
При D>100мс непрерывно200мс target увеличивается на1 до10. Между изменениями
не менее max(500мс, 2*delivery_rtt_us). Высокий обычный RTT без очереди тоже
может включить упаковку — это намеренное поведение.
При D<40мс и пустом Router send_q в течение2с target уменьшается на1.
Промежуточные значения сбрасывают отсчёты high/low, сохраняя target.
Если ACK не продвигался дольше max(2с, 2*delivery_rtt_us, 500мс), рост
и намеренное ожидание выключаются; это состояние не считается свободной полосой.
Предельное время накопления:
budget = clamp((D - 40мс) / 4, 0, 40мс);
hold = min((target - 1)*20мс, budget).
На быстром канале hold сразу равен0, даже пока target постепенно уменьшается.
При150мс полного RTT допустим hold до27,5мс, при200мс и более — до40мс.
Отправка и приём
---------------
Пачка собирается из собственной FIFO до передачи Router. Переданные Router
кодограммы не изменяются и не изымаются из inflight. Подпись и оба уровня
шифрования сохраняются. При отсутствии места остаётся прежний отменяемый
waiter Router; новые кадры/наблюдение/освобождение очереди продолжают drain.
Отправляем при target кадров, достижении лимита байт, несовместимости эпохи
следующего кадра или deadline головы. Новый кадр не продлевает deadline.
40мс — предел адаптивного ожидания (до первого ACK60мс), а не предел
backpressure или полного RTT.
10 кадров — максимальный размер готовой пачки; ожидания180мс ради её наполнения нет.
Прежняя защитная FIFO750 и её удаление старейшего при overflow сохраняются;
данное изменение не обещает жёсткую задержку200мс при остановившемся канале.
sent_ms ставится перед подачей пачки Router. Приёмник передаёт его всем кадрам,
но обновляет оценку сетевого jitter только по первому. Capture PTS остаётся
отдельной временной шкалой каждого кадра; учитываются реальные сетевые паузы,
а собственное накопление пачки не создаёт N фиктивных измерений jitter.
После полной проверки кадры идут в прежние decoder/jitter/headless callbacks.
Первое воспроизведение ждёт накопления не менее200мс аудио в jitter buffer
(10 кадров по20мс), либо сетевого запаса плюс PCM-блок, если это больше.
Захват и отправка начинаются сразу. До первой выдачи PCM нет сигнала перебоя.
После старта действуют прежние адаптивный запас и плавное ускорение излишка;
200мс не закрепляются как постоянная глубина. Повторное накопление после
недостачи и сброс устройства после начала речи используют обычный запас.
Это порог начального заполнения, не ограничение полной сквозной задержки.
cleanup/ENDED/destroy отменяют оба новых таймера и прежний Router waiter до
освобождения FIFO. Доступ к ядру только в uasync; audio capture worker API
не меняется. Счётчики tx/rx кадров и пачек разделены.
Диагностика
-----------
call=info: стартовые target/hold, изменение target с причиной, D, полным RTT,
возрастами и hold; порог JB и фактическая глубина при первом воспроизведении.
call=debug: flow раз в секунду и TX/RX каждой пачки (count, bytes, seq, age).
etcp_route=debug: подтверждение с enqueue->ACK, EWMA и числом попыток.
Ошибки формата/аллокации/таймеров/отправки логируются. Нужны обе новые сборки.
Проверки: test_call_audio_batch (формат и регулятор), test_call (реальная
доставка 10 кадров/неполной пачки и teardown), test_etcp_router (ACK-метрики
после backpressure), test_call_jitter (один arrival на пачку, поздний пакет,
отсутствие декодирования при180мс и старт при200мс),
headless/admission и прежние проверки аудио. Аппаратный эффект проверяется
отдельным звонком и сравнением логов, не выводится из одних unit-тестов.
+124
View File
@@ -0,0 +1,124 @@
Аудиозвонок через BLE: размер пакета по слоям
==========================================
ИСТОРИЧЕСКИЙ СРЕЗ ДО ВВЕДЕНИЯ ПАЧЕК. Текущий формат и размеры описаны
в call_audio_batch.txt. Таблица ниже сохранена для сравнения с исходным звонком.
Срез реализации и измерение: 30.10.2026, Linux -> Samsung A52, кодек 16 кбит/с.
Один Opus-кадр представляет 20 мс звука. В 2021 аудиопакете, отправленном Router
за ACTIVE-интервал 40,571045 с: средний размер Opus 36,939 байта, медиана 37,
минимум 21, максимум 57, p95 44 байта. При постоянных 16 кбит/с за 20 мс было
бы 40 байт; фактический размер закодированного кадра меняется.
Пример ниже: Opus 37 байт, прямой маршрут, одна аудиокодограмма в одном
ETCP DATA-пакете, один BLE-фрагмент. Базовая ETCP ACK-секция содержит count=0;
дополнительных timestamp/measurement/filler/ACK_REQUEST секций нет.
Это расчёт конкретной структуры пакета, не средний расход всех кадров звонка.
Слой / поле Добавлено, байт Всего, байт
-----------------------------------------------------------------------------
Opus 37 37
CALL_MEDIA header 36 73
Router header 57 130
Router E2E: nonce 13 + CRC32 4 + AES-CCM tag 16 33 163
Router: Ed25519 signature 64 227
Router path: node_id 8 + count 1 9 236
Normalizer: длина кодограммы 2 238
ETCP: timestamp 2 + flags 1 3 241
ETCP session: type 1 + sender epoch 8 + peer epoch 8 17 258
ETCP: базовая ACK-секция 8 266
ETCP DATA: type 1 + seq 4 + attempt 2 7 273
ETCP link encryption: nonce 13 + CRC32 4 + tag 16 33 306
BLE flow: номер фрагмента 2 308
BLE frame fragment: type 1 + seq 2 + total 2 + off 2 7 315
ATT: opcode 1 + characteristic handle 2 3 318
L2CAP: length 2 + channel ID 2 4 322
Границы счётчиков:
- 236 байт: Router wire-кодограмма.
- 238 байт: данные normalizer, до обрамления ETCP.
- 306 байт: зашифрованный ETCP-пакет; целый кадр BLE wire TX/RX в журнале.
- 315 байт: значение характеристики GATT; BLE DATA TX/RX в журнале.
- 322 байта: L2CAP PDU, до фрагментации и обрамления Link Layer.
IP/UDP в этом BLE-пути нет. Адаптер ble_etcp передаёт готовый ETCP-пакет
в ble_tx_put без дополнительного собственного заголовка.
Заголовки Link Layer, MIC при включённом шифровании BLE, служебные пакеты,
межпакетные интервалы и радиоповторы в таблицу не включены. Один GATT value
может занимать несколько Link Layer пакетов. HCI не является счётчиком эфира.
Состав CALL_MEDIA header, 36 байт
--------------------------------
subcmd 1 + call_id 8 + track_id 1 + seq 2 + ts_ms 4 + capture_pts_us 8
+ capture_epoch 8 + codec_delay_samples 4 = 36.
Состав Router header, 57 байт
----------------------------
cmd 1 + group_id 8 + dst_node_id 8 + src_node_id 8 + seq 4 + svc_id 1
+ flags 1 + timestamp 2 + reset_id 8 + peer_reset_id 8 + challenge 8 = 57.
Router с защитой и прямым путём добавляет 57 + 33 + 64 + 9 = 163 байта.
CALL и Router вместе добавляют 36 + 163 = 199 байт на каждый Opus-кадр
в текущем формате. Шифрование Router и шифрование ETCP — разные слои:
сквозная защита сервисного содержимого и защита соединения соответственно.
Переменные расходы и подтверждения
----------------------------------
1. ETCP ACK: базовые 8 байт уже включены в таблицу. При N>0 дополнительно
10*N байт записей (seq 4 + attempt 2 + receiver_delay_us 4) и 16 байт точки
приёма (накопленные DATA bytes 8 + receive time 8). Итого ACK 24 + 10*N;
при N=0 — 8 байт. ACK может идти вместе с DATA или отдельным ETCP-пакетом.
2. Дополнительный ETCP timestamp: 5 байт. ACK_REQUEST: 1 байт.
При измерительной серии возможны MEAS_TS 9, MEAS_RESP 13 и FILLER 3 + N
байт. Это условные секции, их нельзя приписывать каждому DATA-пакету.
Для примера таблицы: одна ACK-запись и дополнительный timestamp добавляют
10 + 16 + 5 = 31 байт; кадр BLE становится 337, GATT value — 346 байт.
3. Router ACK обратного аудиопотока: 57 + 9 = 66 байт на прямом пути до
normalizer/ETCP/BLE. Это самостоятельная Router-кодограмма; подтверждения
накопительные, поэтому нет обязательного соотношения один ACK на кадр.
4. BLE ACK: отдельное двухбайтовое GATT value, дополнительно ATT 3 и L2CAP 4.
Обрамление BLE DATA 9 байт к нему не добавляется. ACK накопительный;
возможны повторы. Его расход нельзя добавлять ровно один раз на аудиокадр.
5. Каждый дополнительный Router-hop добавляет 8 байт к списку пути.
После разбиения кадра BLE каждый DATA-фрагмент получает свои 9 байт
BLE-обрамления и собственные ATT/L2CAP-заголовки.
6. Normalizer может объединять кодограммы и разбивать поток на фрагменты:
2 байта длины приходятся на каждую Router-кодограмму; обрамление ETCP —
на каждый передаваемый ETCP-пакет. CALL/Router-заголовки при этом сохраняются.
Поэтому средние размеры всего трафика нельзя получать простым умножением
одного примера на число Opus-кадров. Повторы также считаются отдельно.
Проверка по последнему звонку
----------------------------
Исходящие Linux audio DATA во время ACTIVE:
Opus 74654 байта = 14,721 кбит/с
CALL header 72756 байт = 14,346 кбит/с
Router/crypto/path 329423 байта = 64,957 кбит/с
Всего audio Router DATA 476833 байта = 94,024 кбит/с
Router ACK обратного аудио 109428 байт = 21,578 кбит/с
Все скорости десятичные. Это разные статьи трафика, ACK не входит в DATA.
Полный поток кадров BLE с ETCP и другим трафиком: Linux -> A52 182,179 кбит/с,
A52 -> Linux 170,851 кбит/с. Это не скорость одного аудиопотока и не физическая
скорость PHY. Локальный исходный материал (не включён в репозиторий):
tools/ble-bench/call-diagnosis/gates-20261030/{linux.log,throughput.json}.
Основания в коде
----------------
src/call/call_proto.h: call_pkt_media, CALL_MEDIA_HDR_SIZE, CALL_OPUS_FRAME_MS.
src/call/call.c: call_send_media, call_tx_flush.
src/routing_layer/etcp_router.h: SVC_ROUTE_HDR.
src/routing_layer/etcp_router.c: router_path_append, router_source_send.
src/routing_layer/route_crypto.c: route_crypto_encode.
src/transport_layer/pkt_normalizer.c: etcp_input_ready_cb (длина кодограммы).
src/transport_layer/etcp_connections.h: ETCP_*_SIZE, SC_ENCRYPT_OVERHEAD.
src/transport_layer/etcp_connections.c: etcp_encode_plain, etcp_datagram_send.
src/transport_layer/etcp.c: сборка секций в etcp_request_pkt.
src/transport_layer/secure_channel.h: SC_NONCE_SIZE, SC_CRC32_SIZE, SC_TAG_SIZE, SC_SIGN_SIZE.
tools/cgathui_common/ble_etcp.c: send_packet.
lib/ble/ble_transport.c: pump, HEADER, FLOW_HEADER.
+11 -5
View File
@@ -170,7 +170,8 @@ CALL_PHONE_BUSY 0x09 {call_id:8}
Аудио (`0x3F`):
```
CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, capture_pts_us:8, capture_epoch:8, codec_delay_samples:4, payload}
CALL_MEDIA 0x10 {call_id:8, track_id:1, capture_epoch:8, codec_delay_samples:4, sent_ms:4, count:1, frames[count]}
frame {seq:2, capture_pts_us:8, length:2, opus[length]}
```
- `call_id` — случайный uint64, генерит вызывающий; идентифицирует сессию на обеих сторонах.
@@ -186,11 +187,14 @@ CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, capture_pts_us:8, capt
Надбавка к сервисным данным: nonce(13) + CRC(4) + CCM tag(16) + Ed25519(64) = 97 байт.
- `CALL_MEDIA` переносит только аудио (`track_id=CALL_TRACK_AUDIO`); остальные значения приёмник отклоняет.
Видео имеет отдельный формат и транспорт: `call_video.h`, `call_video_transport.h`, [call_video.txt](call_video.txt).
- Аудио: Opus 48kHz mono, 20ms (960 сэмплов), целевой bitrate32кбит/с, ~50 кадров/с.
- Заголовок медиа — 36 байт с subcmd. `ts_ms` — время отправки для оценки сетевого джиттера;
- Аудио: Opus 48kHz mono, 20ms (960 сэмплов), bitrate16/32/64кбит/с по настройке, ~50 кадров/с.
- Общий заголовок пачки — 27 байт с subcmd, на кадр — ещё12 байт. count=1..10, вся пачка <=1024 байт.
`sent_ms` — время подачи пачки в Router; сетевой jitter измеряется один раз на пачку;
`capture_pts_us` — время первого входного сэмпла в монотонной шкале отправителя, `INT64_MIN` при отсутствии метки.
`capture_epoch` различает разрывы захвата; `codec_delay_samples` передаёт Opus lookahead.
Старый формат не поддерживается; медиа принимается только от пира текущего звонка.
Размер пачки регулируется по полному времени Router enqueue→ACK и возрасту очередей;
быстрый канал не ждёт накопления. Алгоритм и ограничения: [call_audio_batch.txt](call_audio_batch.txt).
## 4. Машина состояний
@@ -447,8 +451,10 @@ Stop дожидается reporter, включая непрерываемый An
Минимум/максимум глубины очереди для GUI собираются отдельно в аудиопотоке за те же десять секунд.
Верхний предел задаётся `call_jitter_max_reserve_ms`: 500–2000 мс, по умолчанию 1000 мс.
Настройка есть в аудиопараметрах desktop и Android и применяется к следующему звонку.
Начальное и повторное накопление ждёт этот запас плюс размер запрошенного PCM-блока;
после первой полной выдачи остаётся целевой запас. Увеличение запаса не прерывает текущую речь.
Первое накопление ждёт минимум200мс аудио либо этот запас плюс размер запрошенного PCM-блока, если это больше.
Захват и отправка идут сразу; до первой выдачи PCM сигнал перебоя не включается.
После старта излишек плавно сокращается обычным регулятором. Повторное накопление ждёт
сетевой запас плюс PCM-блок. Увеличение запаса не прерывает текущую речь.
Ускорение зависит от превышения запаса. При отсутствии входящих кадров обычное воспроизведение
может исчерпать запас: это не жёсткий запрет воспроизведения ниже цели.
Статистика раз в секунду отображается в desktop-окне и Android-экране звонка (под кнопками): глубина, запас, минимум/максимум,
+74
View File
@@ -4,6 +4,80 @@
## Текущие задачи
[+] **Звонок: адаптивные пачки Opus 1..10 по полному времени Router enqueue→ACK** — 30.10.2026.
`call_audio_batch` объединяет кадры в одну подписанную/зашифрованную Router-кодограмму; общий CALL header27,
описание кадра12 байт. seq/PTS сохраняются, capture epoch/codec delay общие; count1..10, пакет до1024 байт.
Router хранит исходный enqueue timestamp через send_q/inflight/ретрансмит и отдаёт полный RTT EWMA,
возраст головы очередей и время последнего ACK через существующий etcp_router_tx_stats.
Полный RTT не вычитается: высокий обычный RTT тоже включает упаковку. Быстрый канал сразу прекращает
намеренное накопление. Рост при >100мс в течение200мс; уменьшение при <40мс без send_q в течение2с.
Старт с target4/deadline60мс до первого пригодного ACK; далее deadline зависит от задержки, максимум40мс.
Десять кадров не требуют ожидания180мс. Первый вывод PCM ждёт минимум200мс в JB; захват/отправка идут сразу.
Сетевой jitter измеряется один раз на пачку. cleanup отменяет оба таймера; счётчики кадров/пачек разделены.
Формат требует обновления обоих участников. Ядро/desktop/Android APK собраны; unit, Router, звонок,
headless/admission и23 voice-проверки PASS. ./check.sh:231/231, proxy/burst/load PASS.
В первом BLE-звонке глитчи были в начале; в логе совпадают с ростом Router RTT/возраста inflight.
Версия со стартом4/JB200 установлена на A52 и Linux GUI, BLE восстановлен; эффект старта требует нового звонка.
Алгоритм/формат: [call_audio_batch.txt](call_audio_batch.txt); прежние размеры: [call_audio_packet_overhead.txt](call_audio_packet_overhead.txt).
[ ] **BLE-звонок 16 кбит/с: задержки Router/ETCP, RTO/pacing и окно по байтам** — 30.10.2026.
В звонке cc621497ae36edfa (40,571 с) прослежены все 2024 исходящих аудиопакета Linux:
Router enqueue → первая успешная передача драйверу BLE, медиана 387,245 мс, p95 676,071 мс, max 895,534 мс.
Средние по стадиям: Router 124,332 мс, normalizer 12,414 мс, ETCP input 28,444 мс, send_q 240,316 мс.
Это задержка до радио, не до воспроизведения. Подтверждено минимум 8,863 с ожидания pacing при готовом BLE;
16,703 с ожидания BLE, включая 3,873 с одновременным pacing; 15,005 с оставлены неклассифицированными.
169 RTO: в 135 случаях ACK пришёл до повтора, в 34 после повтора. BBR получает loss уже при RTO.
Исходящих BGP/Merkle и повторов аудио Router в ACTIVE нет; присутствуют DM/media и ACK.
Добавлена диагностика границ очередей и причин ожидания; алгоритмы RTO/BBR и окно в этом прогоне не менялись.
Следующий шаг: отдельная оценка полного времени ACK для RTO без изменения смысла RTT для BBR, затем A/B;
после этого подбор окна по байтам на пакетах звонка. Проверить обе стороны и влияние подробных логов.
Сборка ядра/GUI, ETCP windows, normalizer, Router, BLE ETCP и ETCP packet link PASS.
Локальный отчёт: `tools/ble-bench/call-diagnosis/gates-20261030/RESULTS.txt` (стенд и логи исключены из Git).
[+] **BLE: проверить пробуждение отправителя при освобождении окна** — 30.10.2026.
Цепочка `wake_writer → ble_etcp.writable → packet_link.state_changed → etcp_conn_wake_sender` проверена;
потерянных пробуждений в проверенных сценариях не обнаружено, рабочий код транспорта не менялся.
Добавлены регрессии MTU 23/517: ACK до/после completion, повтор ACK, повторное заполнение из writable,
tx_limit=1/window=3, чередование 7/1500 байт. Следующий кадр подаётся только callback, без polling/отказа put.
В test_ble_etcp убрана маскирующая пробная отправка при busy; ожидание совместной доставки приглашения и DATA
исправлено с 1000 poll(0) на ожидание результата с deadline: лог подтвердил завершение после ACK через ~15 мс.
BLE transport, BlueZ private-bus, BLE ETCP, packet_link и ETCP packet adapter PASS; проверены счётчики памяти.
В реальном Linux-логе звонка 16 кбит/с: 3985 интервалов от восстановленной готовности до следующего принятого
кадра, медиана 1,201 мс, p95 17,316 мс, max 47,731 мс. Это включает ETCP/BBR, не чистое время callback.
Задержки >200 мс на этом участке не найдены; результат не исключает другие причины глитчей или редкие сбои.
Локальные материалы: `tools/ble-bench/call-diagnosis/call4-resume-audit.json`, `REPORT.txt`, `resume-*.log`.
[ ] **Desktop: завершение GUI после остановки ядра по SIGTERM** — 30.10.2026.
Два наблюдения на Linux: PID 744782 после BLE-звонка и PID 747101 при обновлении desktop ChatGUI.
Повтор 15:00:56–15:01:18: активного звонка не было; при teardown лог `call: destroyed (started=0 ended=0)`.
Значит, завершённый звонок не является обязательным условием; зависимость от BLE пока не установлена.
Воспроизведение наблюдавшегося сценария: запустить обычный `tools/chatgui/build/vibechat`, дождаться
соединений с A52 по BLE и colo2 по сети, открыть монитор соединений, отправить SIGTERM только этому PID.
Ожидается сохранение UI-state, однократная остановка служб/worker и выход процесса без SIGKILL.
Фактически процесс остаётся жив более 22 секунд; в 15:01:07 `ps` показывал 36 потоков, состояние `Ssl`.
Подтверждённая последовательность второго случая:
— 15:00:56.192: BLE закрыт с `reason=transport stopped`; `utun_node: poll stopped ... running=0 requested=1`.
— 15:00:56.196–.199: освобождаются call_audio, radio, chat и транспорт; есть `etcp_router destroyed`.
— с 15:00:56.389 каждые 200 мс повторяется `desktop shutdown queue unavailable; stopping directly`.
— каждый цикл также пытается сохранить `window_geometry`, `window_maximized`, `window_splitter`:
`GUI setting rejected: ... core unavailable`, `UI setting was not queued`, `GUI command rejected`.
GUI-таймер продолжает исполняться; мост к ядру уже отключён. Полный возврат из destroy и завершение
worker не подтверждены: нельзя считать доказанным, что ядро полностью освободилось, а зависло только окно.
Проверить `MainWindow::onShutdownCheck/requestShutdown` (`tools/chatgui/src/mainwindow.cpp`):
сигнал вызывает повторный shutdown через таймер 200 мс, проверка выхода зависит от `m_node->isRunning()`.
Проследить `UtunNode::runLoop`, `requestStop`, `isRunning`, доставку `stopped` и join worker
(`tools/chatgui/transport/utun_node.cpp`), включая ожидание GUI readers и остаток `utun_instance_destroy`.
Это направления проверки, точная точка блокировки пока неизвестна; backtrace не получен
(в первом случае attach gdb был недоступен). Добавить контрольные логи до/после teardown и join,
с generation, идентификатором потока и состояниями stop/running/GUI-detached; проверить фильтры sys/general.
В обоих случаях после ожидания пришлось применить SIGKILL к своему зависшему процессу и запустить GUI снова;
после второго случая новый PID 749434, BLE восстановлен. Это обход, исправления ещё нет.
Лог повтора сохранён локально: `tools/ble-bench/call-diagnosis/gui-shutdown-20261030-150056.log`
(300000 байт, исходный хвост `/tmp/utun-monitor-shutdown-tail.log`; каталог стенда исключён из Git).
Проверка исправления: SIGTERM и «Выход» завершают процесс с открытым/закрытым монитором, после звонка
и без звонка, с BLE и без него; повторный stop не сохраняет UI-state заново и не запускает второй teardown.
Логи должны подтверждать завершение worker и GUI; без зависания, потери ожидающих записей UI и SIGKILL.
[+] **BLE/ETCP: ограничить очередь удалёнными ACK GATT-фрагментов** — 29.10.2026.
BLE version3: циклический ID16bit каждого DATA GATT-value, отдельная ACK Write/Indicate характеристика.
Окно12500байт на направление = фиксированные2Мбит/с ×50ms; локальный completion не возвращает кредит.
+31 -14
View File
@@ -15,7 +15,7 @@
#define STATS_US 5000000ULL
struct ble_stats {
uint64_t since, tx_bytes, rx_bytes, wait_us, max_wait_us;
unsigned tx_frames, rx_frames, fragments, dropped, missing, ack_tx, ack_rx, max_flight;
unsigned tx_frames, rx_frames, fragments, dropped, missing, ack_tx, ack_rx, max_flight, max_frames;
};
struct ble_channel_state {
struct ll_queue *tx, *rx;
@@ -34,8 +34,10 @@ struct ble_connection {
struct ble_channel_state channels[BLE_CHANNEL_COUNT];
unsigned int capacity;
uint16_t pending_bytes;
/* ID монотонен по модулю 65536; кольцо хранит только размеры, payload остаётся у ETCP. */
uint16_t tx_next, tx_acked, rx_last, ack_sent, flight_head, flight_count, flight_bytes, flight_sizes[FLOW_SLOTS];
/* ID монотонен по модулю 65536; кольцо хранит размеры и границы кадров, собственных ретрансляций нет. */
uint16_t tx_next, tx_acked, rx_last, ack_sent, flight_head, flight_count, flight_bytes;
struct { uint16_t size; uint8_t frame_end; } flight[FLOW_SLOTS]; /* Последний фрагмент освобождает слот кадра. */
unsigned tx_frames_pending; /* Принятые DATA, включая очередь и текущий кадр, до удалённого ACK конца. */
uint64_t flight_deadline, ack_due, ack_completed;
void* ack_timer;
int rx_flow_seen, ack_dirty, pending_ack;
@@ -88,10 +90,11 @@ static void log_stats(struct ble_connection* c, uint64_t now) {
queue_entry_count(c->channels[BLE_CHANNEL_INVITE].tx) + !!c->channels[BLE_CHANNEL_INVITE].current,
(unsigned long long)(c->pending ? (now - c->submitted) / 1000 : 0));
DEBUG_DEBUG(CAT, "BLE flow link=%llu flight=%u/%u bytes=%u/%u peak_bytes=%u tx_next=%u acked=%u "
"rx_last=%u ACK_tx=%u ACK_rx=%u ack_dirty=%d ack_pending=%d",
"rx_last=%u ACK_tx=%u ACK_rx=%u ack_dirty=%d ack_pending=%d frames=%u/%u peak_frames=%u",
(unsigned long long)c->id, c->flight_count, (unsigned)FLOW_SLOTS, c->flight_bytes,
BLE_TX_WINDOW_BYTES, s->max_flight, c->tx_next, c->tx_acked, c->rx_last,
s->ack_tx, s->ack_rx, c->ack_dirty, c->pending_ack && c->pending);
s->ack_tx, s->ack_rx, c->ack_dirty, c->pending_ack && c->pending,
c->tx_frames_pending, c->owner->config.tx_window, s->max_frames);
}
memset(s, 0, sizeof(*s)); s->since = now;
}
@@ -228,7 +231,10 @@ static void pump(struct ble_transport* t, struct ble_connection* c) {
c->ack_sent = c->rx_last;
} else if (wire == BLE_WIRE_DATA) {
if (!c->flight_count) c->flight_deadline = c->submitted + DEADLINE_US;
c->flight_sizes[(c->flight_head + c->flight_count++) % FLOW_SLOTS] = len;
unsigned slot = (c->flight_head + c->flight_count++) % FLOW_SLOTS;
struct ble_channel_state* ch = &c->channels[BLE_CHANNEL_DATA];
c->flight[slot].size = len;
c->flight[slot].frame_end = c->pending_bytes && ch->offset + c->pending_bytes == ch->current->len;
c->flight_bytes += len;
if (c->flight_bytes > c->stats.max_flight) c->stats.max_flight = c->flight_bytes;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE DATA TX link=%llu seq=%u type=%u bytes=%zu flight=%u/%u",
@@ -271,7 +277,7 @@ static void tick(void* arg) {
if (!t->timer) { DEBUG_ERROR(CAT, "BLE timer allocation failed"); ble_backend_status(t, 0, "timer allocation failed"); }
}
struct ble_transport* ble_create(struct UASYNC* ua, const struct ble_config* config) {
if (!ua || !config || !config->node_id || config->tx_limit > BLE_QUEUE_MAX ||
if (!ua || !config || !config->node_id || config->tx_limit > BLE_QUEUE_MAX || config->tx_window > BLE_QUEUE_MAX ||
(config->adapter && strlen(config->adapter) >= 64)) {
DEBUG_ERROR(CAT, "BLE create invalid configuration"); return NULL;
}
@@ -279,6 +285,7 @@ struct ble_transport* ble_create(struct UASYNC* ua, const struct ble_config* con
if (!t) { DEBUG_ERROR(CAT, "BLE create allocation failed"); return NULL; }
t->ua = ua; t->config = *config;
if (!t->config.tx_limit) t->config.tx_limit = BLE_QUEUE_MAX;
if (!t->config.tx_window) t->config.tx_window = BLE_TX_WINDOW_FRAMES;
if (config->adapter) { strcpy(t->adapter, config->adapter); t->config.adapter = t->adapter; }
return t;
}
@@ -331,7 +338,8 @@ int ble_channel_tx_ready(struct ble_transport* t, ble_link_id id, enum ble_chann
struct ble_connection* c = lookup(t, id);
struct ble_channel_state* ch = c ? &c->channels[channel] : NULL;
return c && c->ready && (channel != BLE_CHANNEL_DATA ||
(BLE_TX_WINDOW_BYTES - c->flight_bytes >= FLOW_MIN_VALUE && c->flight_count < FLOW_SLOTS)) &&
(c->tx_frames_pending < t->config.tx_window &&
BLE_TX_WINDOW_BYTES - c->flight_bytes >= FLOW_MIN_VALUE && c->flight_count < FLOW_SLOTS)) &&
(unsigned)queue_entry_count(ch->tx) + !!ch->current < (channel == BLE_CHANNEL_DATA ? t->config.tx_limit : 1);
}
int ble_channel_tx_put(struct ble_transport* t, ble_link_id id, enum ble_channel channel, struct ll_entry* e) {
@@ -349,6 +357,12 @@ int ble_channel_tx_put(struct ble_transport* t, ble_link_id id, enum ble_channel
}
/* Без лимита/индекса/consumer put не имеет отказа после проверок и не вызывает чужой callback. */
if (queue_data_put(ch->tx, e) != 0) { DEBUG_ERROR(CAT, "BLE TX queue invariant failed"); return BLE_ERROR; }
if (channel == BLE_CHANNEL_DATA) {
c->tx_frames_pending++;
if (c->tx_frames_pending > c->stats.max_frames) c->stats.max_frames = c->tx_frames_pending;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE frame queued link=%llu frames=%u/%u bytes=%u",
(unsigned long long)id, c->tx_frames_pending, t->config.tx_window, e->len);
}
if (!ble_channel_tx_ready(t, id, channel)) ch->blocked = 1;
pump(t, c); return BLE_OK;
}
@@ -389,9 +403,9 @@ void ble_backend_opened(struct ble_transport* t, ble_link_id id, unsigned int mt
c->opened = 1;
DEBUG_INFO(CAT, "BLE GATT open link=%llu mtu=%u fragment_payload=%u outgoing=%d DATA=without_response/notify INVITE=request/indicate tx_limit=%u",
(unsigned long long)id, mtu, c->capacity - HEADER - FLOW_HEADER, c->outgoing, t->config.tx_limit);
DEBUG_INFO(CAT, "BLE flow configured link=%llu rate_bits=%u window_ms=%u window_bytes=%u "
DEBUG_INFO(CAT, "BLE flow configured link=%llu window_frames=%u window_bytes=%u "
"ACK=without_response/notify ACK_interval_us=%u ACK_repeat_us=%u id_bits=16",
(unsigned long long)id, BLE_TX_RATE_BITS, BLE_TX_WINDOW_MS, BLE_TX_WINDOW_BYTES, BLE_ACK_INTERVAL_US, BLE_ACK_REPEAT_US);
(unsigned long long)id, t->config.tx_window, BLE_TX_WINDOW_BYTES, BLE_ACK_INTERVAL_US, BLE_ACK_REPEAT_US);
if (c->outgoing) {
c->control[0] = HELLO; write64(c->control + 1, t->config.node_id); write64(c->control + 9, c->peer);
c->control_pending = 1;
@@ -424,14 +438,17 @@ void ble_backend_received(struct ble_transport* t, ble_link_id id, enum ble_wire
(unsigned long long)id, ack, c->tx_acked, c->tx_next, c->flight_count);
close_link(t, c, "ACK outside flight"); return;
}
unsigned bytes = 0;
unsigned bytes = 0, frames = 0;
for (unsigned i = 0; i < advance; i++) {
bytes += c->flight_sizes[c->flight_head]; c->flight_head = (c->flight_head + 1) % FLOW_SLOTS;
bytes += c->flight[c->flight_head].size; frames += c->flight[c->flight_head].frame_end;
c->flight_head = (c->flight_head + 1) % FLOW_SLOTS;
}
c->tx_frames_pending -= frames;
c->flight_count -= advance; c->flight_bytes -= bytes; c->tx_acked = ack; c->stats.ack_rx++;
c->flight_deadline = c->flight_count ? get_time_us() + DEADLINE_US : 0;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE ACK RX link=%llu id=%u released=%u/%u flight=%u/%u", (unsigned long long)id,
ack, advance, bytes, c->flight_count, c->flight_bytes);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE ACK RX link=%llu id=%u released=%u/%u flight=%u/%u frames_released=%u frames=%u/%u",
(unsigned long long)id, ack, advance, bytes, c->flight_count, c->flight_bytes,
frames, c->tx_frames_pending, t->config.tx_window);
wake_writer(t, c, BLE_CHANNEL_DATA); pump(t, c); return;
}
enum ble_channel channel = wire == BLE_WIRE_DATA ? BLE_CHANNEL_DATA : BLE_CHANNEL_INVITE;
+4 -5
View File
@@ -20,10 +20,8 @@ extern "C" {
#define BLE_ENDPOINT_MAX 256
#define BLE_LINKS_MAX 16
#define BLE_QUEUE_MAX 64
/* Расчётная скорость фиксирована; окно включает DATA GATT-values с заголовками. */
#define BLE_TX_RATE_BITS 2000000u
#define BLE_TX_WINDOW_MS 50u
#define BLE_TX_WINDOW_BYTES (BLE_TX_RATE_BITS / 8u * BLE_TX_WINDOW_MS / 1000u)
#define BLE_TX_WINDOW_FRAMES 3u /* Целые DATA от tx_put до ACK последнего фрагмента; баланс скорости/RTT на A52/BlueZ. */
#define BLE_TX_WINDOW_BYTES 12500u /* Дополнительный предел GATT-байтов; не оценка скорости/задержки. */
#define BLE_ACK_INTERVAL_US 15000u
#define BLE_ACK_REPEAT_US 100000u /* Повтор последнего ACK при отсутствии нового DATA. */
struct ble_transport;
@@ -52,6 +50,7 @@ struct ble_config {
struct ble_callbacks callbacks;
void* arg; /* Заимствованный контекст callbacks, живёт до destroy. */
unsigned tx_limit; /* Целых TX-пакетов, включая фрагментируемый: 0=64, 1..64. */
unsigned tx_window; /* DATA в очереди/отправке/ожидании удалённого ACK: 0=BLE_TX_WINDOW_FRAMES, 1..64. */
};
/* Создать остановленный transport. ua/config заимствованы только ua; возвращает handle или NULL с логом. */
struct ble_transport* ble_create(struct UASYNC* ua, const struct ble_config* config);
@@ -74,7 +73,7 @@ void ble_disconnect(struct ble_transport* transport, ble_link_id link);
/* Принять целый ll_entry в TX. При 0 владение передано; при любом отказе entry остаётся вызывающему.
* entry должен быть вне очереди, dgram!=NULL, len=1..1500. Заполнение: WOULD_BLOCK, затем writable. */
int ble_tx_put(struct ble_transport* transport, ble_link_id link, struct ll_entry* entry);
/* Снимок готовности принять пакет; заполнение учитывает текущую фрагментацию.
/* Снимок готовности принять пакет; учитывает очередь и кадры до ACK последнего фрагмента.
* После заполнения writable вызывается при освобождении места, без обязательного отказа tx_put. */
int ble_tx_ready(struct ble_transport* transport, ble_link_id link);
/* Извлечь целый кадр из RX или NULL; вызывающий освобождает queue_dgram_free + queue_entry_free. */
+5 -2
View File
@@ -137,9 +137,10 @@ static int acquire(const char* device, int channel) {
"AcquireNotify", replied, &reply, "a{sv}", 2, "device", "o", device, "mtu", "q", 23) >= 0);
poll_all(); CHECK(reply.done && !reply.failed && reply.fd >= 0); sd_bus_slot_unref(reply.slot); return reply.fd;
}
static void write_value(const char* device, const uint8_t* data, size_t len, int invitation) {
static void write_value(const char* device, const uint8_t* data, size_t len, int channel) {
struct reply reply = {0}; sd_bus_message* m = NULL;
CHECK(sd_bus_message_new_method_call(bus, &m, application, invitation ? ROOT "/service0/invite" : ROOT "/service0/rx", "org.bluez.GattCharacteristic1", "WriteValue") >= 0);
const char* path = channel == 2 ? ROOT "/service0/ack" : channel ? ROOT "/service0/invite" : ROOT "/service0/rx";
CHECK(sd_bus_message_new_method_call(bus, &m, application, path, "org.bluez.GattCharacteristic1", "WriteValue") >= 0);
CHECK(sd_bus_message_append_array(m, 'y', data, len) >= 0);
CHECK(sd_bus_message_append(m, "a{sv}", 2, "device", "o", device, "mtu", "q", 23) >= 0);
CHECK(sd_bus_call_async(bus, &reply.slot, m, replied, &reply, 1000000) >= 0); sd_bus_message_unref(m);
@@ -219,6 +220,8 @@ int main(void) {
CHECK(recv(fd1, data, sizeof(data), MSG_DONTWAIT) == (part == 22 ? 18 : 20));
CHECK(data[8] == part && data[9] == 0xd4);
}
/* Flow ACK последнего DATA-фрагмента, независимо от ATT confirmation канала INVITE. */
write_value(ADAPTER "/dev_A", data, 2, 2);
CHECK(send(invite2, "\1", 1, MSG_NOSIGNAL) == 1); poll_all();
}
CHECK(ack_values >= 2);
+117 -3
View File
@@ -110,6 +110,78 @@ static struct ll_entry* frame(unsigned int size, uint8_t value) {
struct ll_entry* e = ll_alloc_lldgram(size); CHECK(e); e->len = size; memset(e->dgram, value, size); return e;
}
static void release(struct ll_entry* e) { queue_dgram_free(e); queue_entry_free(e); }
struct resume_writer { struct ble_transport* radio; unsigned sent, wakes, goal; int fragmented; };
/* Следующий кадр подаёт только writable: ни polling tx_ready, ни пробный отказ не маскируют потерянное пробуждение. */
static void resume_write(void* arg, ble_link_id id) {
struct resume_writer* w = arg; w->wakes++;
CHECK(ble_tx_ready(w->radio, id));
if (w->sent == w->goal) return;
unsigned size = w->fragmented && w->sent % 2 ? 1500 : 7;
CHECK(!ble_tx_put(w->radio, id, frame(size, w->sent))); w->sent++;
}
/* ACK до completion не освобождает занятую локальную очередь; обратный порядок ждёт ACK окна из одного кадра. */
static void exercise_resume_order(unsigned test_mtu, int ack_first) {
mtu = test_mtu; hold = hold_ack = drop = reject_peer = 0;
struct UASYNC* ua = uasync_create(); CHECK(ua);
struct resume_writer writer = {0};
struct ble_config cfg = {.node_id = 1, .tx_limit = 1, .tx_window = 1, .arg = &writer};
cfg.callbacks.writable = resume_write;
struct ble_transport* a = ble_create(ua, &cfg); writer.radio = a;
cfg.node_id = 2; cfg.arg = NULL; cfg.callbacks.writable = NULL;
struct ble_transport* b = ble_create(ua, &cfg); CHECK(a && b && !ble_start(a) && !ble_start(b)); drain(ua);
struct ble_endpoint ep = {"1"}; ble_link_id id = ble_connect(a, &ep, 2); CHECK(id); drain(ua);
ble_link_id other = incoming_ids[1]; hold = hold_ack = 1;
writer = (struct resume_writer){.radio = a, .goal = 8};
CHECK(!ble_tx_put(a, id, frame(7, 0))); writer.sent = 1;
for (unsigned i = 0; i < writer.goal; i++) {
struct fake_link* f = slot(backends[0], id); CHECK(f && f->pending && f->channel == BLE_WIRE_DATA);
uint8_t ack[2] = {f->data[0], f->data[1]};
CHECK(!ble_tx_ready(a, id) && writer.wakes == i);
ble_backend_received(b, other, f->channel, f->data, f->len);
if (ack_first) ble_backend_received(a, id, BLE_WIRE_ACK, ack, sizeof(ack));
else { f->pending = 0; ble_backend_sent(a, id, 1); }
CHECK(!ble_tx_ready(a, id) && writer.wakes == i);
if (ack_first) { f->pending = 0; ble_backend_sent(a, id, 1); }
else ble_backend_received(a, id, BLE_WIRE_ACK, ack, sizeof(ack));
CHECK(writer.wakes == i + 1);
ble_backend_received(a, id, BLE_WIRE_ACK, ack, sizeof(ack)); CHECK(writer.wakes == i + 1);
struct ll_entry* e = ble_rx_get(b, other); CHECK(e && e->len == 7 && e->dgram[0] == i); release(e);
}
CHECK(writer.sent == writer.goal && ble_tx_ready(a, id));
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "PASS BLE resume order: MTU=%u ACK_first=%d wakes=%u frames=%u; no refused put/polling",
test_mtu, ack_first, writer.wakes, writer.sent);
ble_destroy(a); ble_destroy(b); uasync_destroy(ua, 0); hold = hold_ack = 0;
}
/* Production tx_limit=1/window=3: только callback подаёт следующие короткие и фрагментированные кадры. */
static void exercise_resume_stream(unsigned test_mtu) {
mtu = test_mtu; hold = hold_ack = drop = reject_peer = 0;
struct UASYNC* ua = uasync_create(); CHECK(ua);
struct resume_writer writer = {0};
struct ble_config cfg = {.node_id = 1, .tx_limit = 1, .tx_window = 3, .arg = &writer};
cfg.callbacks.writable = resume_write;
struct ble_transport* a = ble_create(ua, &cfg); writer.radio = a;
cfg.node_id = 2; cfg.arg = NULL; cfg.callbacks.writable = NULL;
struct ble_transport* b = ble_create(ua, &cfg); CHECK(a && b && !ble_start(a) && !ble_start(b)); drain(ua);
struct ble_endpoint ep = {"1"}; ble_link_id id = ble_connect(a, &ep, 2); CHECK(id); drain(ua);
ble_link_id other = incoming_ids[1];
writer = (struct resume_writer){.radio = a, .goal = 24, .fragmented = 1};
CHECK(!ble_tx_put(a, id, frame(7, 0))); writer.sent = 1;
uint64_t started = get_time_us(); unsigned received = 0;
while ((received < writer.goal || writer.wakes < writer.goal) && get_time_us() - started < 3000000) {
uasync_poll(ua, 10);
struct ll_entry* e;
while ((e = ble_rx_get(b, other))) {
CHECK(e->len == (received % 2 ? 1500 : 7));
for (unsigned i = 0; i < e->len; i++) CHECK(e->dgram[i] == received);
received++; release(e);
}
}
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "BLE resume stream: MTU=%u TX=%u RX=%u wakes=%u elapsed_us=%llu",
test_mtu, writer.sent, received, writer.wakes, (unsigned long long)(get_time_us() - started));
CHECK(writer.sent == writer.goal && received == writer.goal && writer.wakes == writer.goal);
CHECK(ble_tx_ready(a, id));
ble_destroy(a); ble_destroy(b); uasync_destroy(ua, 0);
}
/* Непрерывный RX при ACK completion >15ms не должен вытеснять встречный DATA. */
static void exercise_ack_fairness(void) {
mtu = 517; hold = drop = reject_peer = hold_ack = 0; ack_delay_us = 0;
@@ -141,7 +213,7 @@ static void exercise(unsigned int test_mtu) {
struct UASYNC* ua = uasync_create(); CHECK(ua);
struct ble_transport* t[3];
for (int i = 0; i < 3; i++) {
struct ble_config cfg = {0}; cfg.node_id = i + 1; cfg.company_id = 0xffff;
struct ble_config cfg = {.tx_window = BLE_QUEUE_MAX}; cfg.node_id = i + 1; cfg.company_id = 0xffff;
cfg.callbacks.validate = validate; cfg.callbacks.connection = connection; cfg.callbacks.writable = writable;
t[i] = ble_create(ua, &cfg); CHECK(t[i] && !ble_start(t[i]));
}
@@ -208,7 +280,8 @@ static void exercise(unsigned int test_mtu) {
static void exercise_flow(void) {
mtu = 517; hold = drop = reject_peer = hold_ack = 0;
struct UASYNC* ua = uasync_create(); CHECK(ua);
struct ble_config cfg = {.node_id = 1}; cfg.callbacks.writable = writable; cfg.callbacks.connection = connection;
struct ble_config cfg = {.node_id = 1, .tx_window = BLE_QUEUE_MAX};
cfg.callbacks.writable = writable; cfg.callbacks.connection = connection;
struct ble_transport* a = ble_create(ua, &cfg); cfg.node_id = 2;
struct ble_transport* b = ble_create(ua, &cfg); CHECK(a && b && !ble_start(a) && !ble_start(b)); drain(ua);
struct ble_endpoint ep = {"1"}; ble_link_id id = ble_connect(a, &ep, 2); CHECK(id); drain(ua);
@@ -254,7 +327,7 @@ static void exercise_flow(void) {
(unsigned long long)(get_time_us() - recovery_started));
drain(ua);
/* Больше полного оборота wire ID; новые данные действительно приходят, старые ACK не открывают окно. */
unsigned sent = 0, delivered = 0; deadline = get_time_us() + 10000000;
unsigned sent = 0, delivered = 0; deadline = get_time_us() + 30000000;
while (delivered < 70000 && get_time_us() < deadline) {
while (sent < 70000 && ble_tx_ready(a, id)) { CHECK(!ble_tx_put(a, id, frame(1, sent))); sent++; }
uasync_poll(ua, 0);
@@ -273,11 +346,52 @@ static void exercise_flow(void) {
ble_destroy(a); ble_destroy(b); uasync_destroy(ua, 0);
puts("BLE flow PASS (12500-byte duplex bound, ACK >=15ms, duplicate ACK, resume, 70000 IDs, invalid ACK, timeout, teardown)");
}
/* Локальное завершение и ACK середины кадра не освобождают окно; ACK конца освобождает ровно один слот. */
static void exercise_frame_window(unsigned test_mtu) {
mtu = test_mtu; hold = drop = reject_peer = hold_ack = 0;
struct UASYNC* ua = uasync_create(); CHECK(ua);
struct ble_config cfg = {.node_id = 1, .tx_window = 2}; cfg.callbacks.writable = writable;
struct ble_transport* a = ble_create(ua, &cfg); cfg.node_id = 2;
struct ble_transport* b = ble_create(ua, &cfg); CHECK(a && b && !ble_start(a) && !ble_start(b)); drain(ua);
struct ble_endpoint ep = {"1"}; ble_link_id id = ble_connect(a, &ep, 2); CHECK(id); drain(ua);
ble_link_id other = incoming_ids[1]; hold_ack = 1;
CHECK(!ble_tx_put(a, id, frame(1500, 0x41))); drain(ua);
struct fake_link* receiver = slot(backends[1], other); CHECK(receiver && receiver->ack_held);
uint16_t first_end = receiver->ack_id, partial = first_end - 1;
uint8_t ack[2] = {partial >> 8, partial};
ble_backend_received(a, id, BLE_WIRE_ACK, ack, sizeof(ack));
CHECK(!ble_tx_put(a, id, frame(17, 0x42))); drain(ua);
CHECK(!ble_tx_ready(a, id));
struct ll_entry* e = frame(7, 0x43);
CHECK(ble_tx_put(a, id, e) == BLE_WOULD_BLOCK && !e->owner);
unsigned before_writable = writable_count;
ack[0] = first_end >> 8; ack[1] = first_end;
ble_backend_received(a, id, BLE_WIRE_ACK, ack, sizeof(ack));
CHECK(ble_tx_ready(a, id) && writable_count > before_writable);
CHECK(!ble_tx_put(a, id, e) && !ble_tx_ready(a, id));
ble_backend_received(a, id, BLE_WIRE_ACK, ack, sizeof(ack)); CHECK(!ble_tx_ready(a, id));
CHECK(!ble_channel_tx_put(a, id, BLE_CHANNEL_INVITE, frame(3, 0x44))); drain(ua);
e = ble_channel_rx_get(b, other, BLE_CHANNEL_INVITE); CHECK(e && e->len == 3); release(e);
hold_ack = 0; schedule(backends[1]); drain(ua);
unsigned sizes[] = {1500, 17, 7};
for (unsigned i = 0; i < 3; i++) {
e = ble_rx_get(b, other); CHECK(e && e->len == sizes[i] && e->dgram[0] == 0x41 + i); release(e);
}
CHECK(!ble_rx_get(b, other) && ble_tx_ready(a, id));
ble_destroy(a); ble_destroy(b); uasync_destroy(ua, 0);
printf("BLE frame window PASS (MTU=%u, two frames, partial/final/duplicate ACK, writable, independent INVITE)\n", test_mtu);
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO);
size_t before = u_get_allocated_count();
for (unsigned i = 0; i < 2; i++) {
exercise_resume_order(i ? 517 : 23, 0); exercise_resume_order(i ? 517 : 23, 1);
exercise_resume_stream(i ? 517 : 23); CHECK(u_get_allocated_count() == before);
}
exercise_ack_fairness(); CHECK(u_get_allocated_count() == before);
exercise(23); exercise(247); exercise(517);
exercise_frame_window(23); exercise_frame_window(517);
exercise_flow();
CHECK(u_get_allocated_count() == before); puts("BLE transport tests PASS (MTU 23/247/517, duplex, loss, backpressure, peers, teardown)"); return 0;
}
+4
View File
@@ -208,6 +208,8 @@ utun_CORE_SOURCES = \
dm/dm_mailbox_media.c \
dm/dm_mailbox_media.h \
call/call.c \
call/call_audio_batch.c \
call/call_audio_batch.h \
call/call_video_rate.c \
call/call_video_rate.h \
call/call_video.c \
@@ -402,6 +404,8 @@ libutun_a_SOURCES = \
dm/dm_mailbox_media.c \
dm/dm_mailbox_media.h \
call/call.c \
call/call_audio_batch.c \
call/call_audio_batch.h \
call/call_video_rate.c \
call/call_video_rate.h \
call/call_video.c \
+142 -90
View File
@@ -40,6 +40,16 @@
#define CALL_ID "call"
/* Локальный кадр до упаковки: время создания не зависит от сетевого wire timestamp пачки. */
struct call_audio_pending {
uint64_t created_us;
struct call_capture_time timing;
uint16_t seq;
};
static void call_tx_flush(struct call_session* s);
static void call_audio_batch_tick(void* arg);
/* ───────────────────────────────────────────────────────────────────────── */
static struct call_ctx* call_of(struct UTUN_INSTANCE* inst) {
@@ -140,6 +150,7 @@ static struct call_session* call_session_new(struct call_ctx* ctx, uint64_t call
s->state = CALL_ST_IDLE;
s->started_tb = get_time_tb();
s->ctx = ctx;
call_audio_batch_init(&s->audio_batch, get_time_us());
s->tx_q = queue_new(ctx->inst->ua, 0, 0, 0, "call_txq");
if (!s->tx_q) { queue_entry_free(qe); return NULL; }
// При провале queue_data_put_with_index сам освобождает qe (вместе с s), поэтому
@@ -161,6 +172,9 @@ static void call_session_free_cb(void* arg) {
* дренировать tx_q (без отправки). Безопасно вызывать из uasync-потока. */
static void call_session_cleanup(struct call_ctx* ctx, struct call_session* s) {
call_video_transport_stop(s);
if (s->audio_batch_timer) uasync_cancel_timeout(ctx->inst->ua, s->audio_batch_timer);
if (s->audio_flush_timer) uasync_cancel_timeout(ctx->inst->ua, s->audio_flush_timer);
s->audio_batch_timer = s->audio_flush_timer = NULL;
etcp_router_cancel_send_ready(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL_AUDIO, &s->send_waiter);
if (s->tx_q) {
struct ll_entry* e;
@@ -417,6 +431,9 @@ static void call_activate(struct call_session* s) {
if (s->state == CALL_ST_ACTIVE || s->state == CALL_ST_ENDED) return;
struct call_ctx* ctx = s->ctx;
s->state = CALL_ST_ACTIVE;
call_audio_batch_tick(s);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio batch startup call=%llx target=%u hold_us=%u",
(unsigned long long)s->call_id, s->audio_batch.target, s->audio_batch.hold_us);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: accepted id=%016llx -> ACTIVE (router delivery)", CALL_ID,
(unsigned long long)s->call_id);
call_event_id(ctx, CHAT_EVT_CALL_ACCEPTED, s->call_id, 0);
@@ -513,8 +530,8 @@ static void call_post_stats(struct call_session* s) {
uint16_t rtt = rtt_tb / 10; /* ETCP хранит RTT в 0.1мс; CALL_STATS передаёт миллисекунды. */
uint32_t oldest_ms = 0;
if (s->tx_q && s->tx_q->head) {
struct call_pkt_media oldest; memcpy(&oldest, s->tx_q->head->dgram + 1, sizeof(oldest));
oldest_ms = (uint32_t)(get_time_tb() / 10) - oldest.ts_ms;
struct call_audio_pending *oldest = (struct call_audio_pending*)s->tx_q->head->data;
oldest_ms = (uint32_t)((get_time_us() - oldest->created_us) / 1000);
}
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call audio TX queue id=%016llx drained=%u queued=%zu oldest_ms=%u wait_avg_max_ms=%llu/%u",
(unsigned long long)s->call_id, s->tx_wait_frames, s->tx_q ? (size_t)queue_entry_count(s->tx_q) : 0, oldest_ms,
@@ -756,46 +773,33 @@ static void call_handle_hangup(struct call_ctx* ctx, uint64_t from, const uint8_
}
static void call_handle_media(struct call_ctx* ctx, uint64_t from, const uint8_t* b, size_t n) {
if (n <= CALL_MEDIA_HDR_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "call media truncated bytes=%zu", n); return; }
uint64_t call_id; memcpy(&call_id, b + 1, 8);
struct call_session* s = call_session_find(ctx, call_id);
if (!s || s->state != CALL_ST_ACTIVE) {
/* медиа вне ACTIVE — игнорируем (гонка с завершением) */
return;
struct call_audio_batch_view batch;
if (call_audio_batch_parse(b, n, &batch)) return;
struct call_pkt_media *h = &batch.header;
struct call_session* s = call_session_find(ctx, h->call_id);
if (!s || s->state != CALL_ST_ACTIVE) return;
if (from != s->peer_node_id) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio batch wrong peer=%llx expected=%llx",
(unsigned long long)from, (unsigned long long)s->peer_node_id); return;
}
struct call_pkt_media packet;
memcpy(&packet, b, sizeof(packet));
if (from != s->peer_node_id || packet.track_id != CALL_TRACK_AUDIO || packet.codec_delay_samples > CALL_OPUS_SAMPLE_RATE
|| (packet.capture_pts_us != INT64_MIN && (packet.capture_pts_us < 0 || packet.capture_pts_us > INT64_MAX / 2
|| !packet.capture_epoch))) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call media invalid peer=%016llx expected=%016llx track=%u pts=%lld epoch=%llu delay=%u",
(unsigned long long)from, (unsigned long long)s->peer_node_id, packet.track_id,
(long long)packet.capture_pts_us, (unsigned long long)packet.capture_epoch, packet.codec_delay_samples);
return;
}
struct call_capture_time timing = {packet.capture_pts_us, packet.capture_epoch, packet.codec_delay_samples};
s->c_rx_frames++;
++s->rx_batches;
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "audio batch RX call=%llx frames=%u bytes=%zu first_seq=%u sent_ms=%u",
(unsigned long long)s->call_id, h->count, n, batch.frames[0].header.seq, h->sent_ms);
s->last_media_tb = get_time_tb();
uint16_t seq = 0;
uint32_t ts_ms = 0;
memcpy(&seq, b + 10, 2);
memcpy(&ts_ms, b + 12, 4);
/* аудио-движок (декод + джиттер-буфер) — если зарегистрирован */
const struct call_audio_ops* audio = ctx->inst->call_audio_ops;
if (audio && audio->on_media)
audio->on_media(ctx->inst, call_id, seq, ts_ms, b + CALL_MEDIA_HDR_SIZE, (int)(n - CALL_MEDIA_HDR_SIZE), &timing);
/* релей Opus-кадра потребителю (headless-аудио-сокет) */
if (ctx->media_recv_fn)
ctx->media_recv_fn(ctx->inst, call_id, b + CALL_MEDIA_HDR_SIZE,
(int)(n - CALL_MEDIA_HDR_SIZE), ctx->media_recv_arg);
/* watchdog: пере-взводим таймер no-traffic на каждый принятый кадр */
if (s->traffic_timer) { uasync_cancel_timeout(ctx->inst->ua, s->traffic_timer); s->traffic_timer = NULL; }
for (unsigned i = 0; i < h->count && s->state == CALL_ST_ACTIVE; i++) {
const struct call_audio_frame_view *f = &batch.frames[i];
struct call_capture_time timing = {f->header.capture_pts_us, h->capture_epoch, h->codec_delay_samples};
s->c_rx_frames++;
const struct call_audio_ops* audio = ctx->inst->call_audio_ops;
if (audio && audio->on_media)
audio->on_media(ctx->inst, s->call_id, f->header.seq, h->sent_ms, f->opus, f->header.length, &timing, i == 0);
if (ctx->media_recv_fn && s->state == CALL_ST_ACTIVE)
ctx->media_recv_fn(ctx->inst, s->call_id, f->opus, f->header.length, ctx->media_recv_arg);
}
if (s->state != CALL_ST_ACTIVE) return;
if (s->traffic_timer) uasync_cancel_timeout(ctx->inst->ua, s->traffic_timer);
s->traffic_timer = uasync_set_timeout(ctx->inst->ua, CALL_NO_TRAFFIC_TB, s, call_traffic_timeout_cb, "call_traffic");
(void)from;
if (!s->traffic_timer) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio RX watchdog allocation failed");
}
/* ─────────────────── приём (etcp_router) ─────────────────── */
@@ -1001,77 +1005,125 @@ static void call_tx_drain_cb(struct ll_queue* q, void* arg) {
call_tx_flush(s);
}
/* Дренируем tx_q, пока роутер готов; иначе — одноразовый waiter (re-arm в drain_cb). */
/* Deadline относится к возрасту головы; новые кадры не отодвигают срок отправки. */
static void call_audio_flush_timer(void* arg) {
struct call_session *s = arg; s->audio_flush_timer = NULL; call_tx_flush(s);
}
static void call_audio_batch_tick(void* arg) {
struct call_session *s = arg; s->audio_batch_timer = NULL;
if (s->state != CALL_ST_ACTIVE) return;
uint64_t now = get_time_us();
struct ETCP_ROUTER_TX_STATS net = {0};
int found = etcp_router_tx_stats(s->ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL_AUDIO, now, &net) == 0;
if (call_audio_batch_step(&s->audio_batch, now, found ? &net : NULL)) s->audio_batch.hold_us = 0;
if (now - s->audio_report_us >= 1000000) {
s->audio_report_us = now;
DEBUG_DEBUG(DEBUG_CATEGORY_CALL,
"audio batch flow call=%llx target=%u hold_us=%u router_rtt_us=%llu queued=%u/%llu inflight=%u/%llu "
"ack_bytes_500ms=%llu local_frames=%d tx_frames=%u tx_batches=%u rx_frames=%u rx_batches=%u",
(unsigned long long)s->call_id, s->audio_batch.target, s->audio_batch.hold_us,
(unsigned long long)net.delivery_rtt_us, net.queued_packets, (unsigned long long)net.queued_age_us,
net.inflight_packets, (unsigned long long)net.inflight_age_us, (unsigned long long)net.window.bytes,
s->tx_q ? queue_entry_count(s->tx_q) : 0, s->c_tx_frames, s->tx_batches, s->c_rx_frames, s->rx_batches);
}
call_tx_flush(s);
if (s->state != CALL_ST_ACTIVE) return;
s->audio_batch_timer = uasync_set_timeout(s->ctx->inst->ua, 500, s, call_audio_batch_tick, "audio_batch");
if (!s->audio_batch_timer) {
s->audio_batch.hold_us = 0;
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio batch timer allocation failed; immediate TX");
}
}
/* Пачка формируется до Router: принятые им кодограммы не изменяются. */
static void call_tx_flush(struct call_session* s) {
struct call_ctx* ctx = s->ctx;
if (s->state == CALL_ST_ENDED) return;
/* гарантируем router conn — иначе has_room/on_send_ready не видят его */
etcp_router_conn_get(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL_AUDIO);
while (s->tx_q && s->tx_q->head) {
if (s->state != CALL_ST_ACTIVE || !s->tx_q || !s->tx_q->head) return;
if (s->audio_flush_timer) { uasync_cancel_timeout(ctx->inst->ua, s->audio_flush_timer); s->audio_flush_timer = NULL; }
if (!etcp_router_conn_get(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL_AUDIO)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio router channel unavailable; retaining frames"); return;
}
while (s->state == CALL_ST_ACTIVE && s->tx_q && s->tx_q->head) {
if (!etcp_router_send_q_has_room(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL_AUDIO)) {
etcp_router_on_send_ready(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL_AUDIO,
&s->send_waiter, call_tx_drain_cb, s);
return;
}
struct ll_entry* e = queue_data_get(s->tx_q);
struct call_pkt_media packet; memcpy(&packet, e->dgram + 1, sizeof(packet));
uint32_t wait_ms = (uint32_t)(get_time_tb() / 10) - packet.ts_ms;
++s->tx_wait_frames; s->tx_wait_total_ms += wait_ms;
if (wait_ms > s->tx_wait_max_ms) s->tx_wait_max_ms = wait_ms;
if (etcp_route_send(ctx->inst, s->group_id, s->peer_node_id, e, 1, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT) == 0) {
s->c_tx_frames++;
if (packet.capture_pts_us != INT64_MIN) call_video_audio_sent(s, packet.capture_pts_us + 20000);
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: media enqueue failed id=%016llx", CALL_ID, (unsigned long long)s->call_id);
struct call_audio_pending *first = (struct call_audio_pending*)s->tx_q->head->data;
struct call_audio_batch_view batch = {.header = {.subcmd = CALL_SUBCMD_MEDIA, .call_id = s->call_id,
.track_id = CALL_TRACK_AUDIO, .capture_epoch = first->timing.epoch,
.codec_delay_samples = first->timing.delay_samples, .sent_ms = (uint32_t)(get_time_tb() / 10)}};
size_t size = CALL_MEDIA_HDR_SIZE;
struct ll_entry *next = s->tx_q->head;
while (next && batch.header.count < s->audio_batch.target) {
struct call_audio_pending *p = (struct call_audio_pending*)next->data;
if (p->timing.epoch != first->timing.epoch || p->timing.delay_samples != first->timing.delay_samples
|| size + CALL_AUDIO_FRAME_HDR_SIZE + next->len > CALL_MEDIA_MAX_BYTES) break;
batch.frames[batch.header.count++] = (struct call_audio_frame_view){
.header = {p->seq, p->timing.pts_us, next->len}, .opus = next->dgram};
size += CALL_AUDIO_FRAME_HDR_SIZE + next->len; next = next->next;
}
uint64_t now = get_time_us(), age = now - first->created_us;
if (!next && batch.header.count < s->audio_batch.target && age < s->audio_batch.hold_us) {
s->audio_flush_timer = uasync_set_timeout(ctx->inst->ua, (s->audio_batch.hold_us - age + 199) / 100,
s, call_audio_flush_timer, "audio_batch_flush");
if (s->audio_flush_timer) return;
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio flush timer allocation failed; sending incomplete batch");
}
struct ll_entry *wire = ll_alloc_lldgram(1 + size);
if (!wire) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio batch allocation failed bytes=%zu", size); return; }
wire->dgram[0] = ETCP_RT_ID_CALL_AUDIO;
size_t encoded = 0;
if (call_audio_batch_encode(&batch, wire->dgram + 1, size, &encoded)) {
queue_dgram_free(wire); queue_entry_free(wire); return;
}
wire->len = 1 + encoded;
unsigned count = batch.header.count;
int64_t last_pts = batch.frames[count - 1].header.capture_pts_us;
/* Снять исходные кадры до синхронных callbacks Router, которые могут завершить звонок. */
for (unsigned i = 0; i < count; i++) {
struct ll_entry *e = queue_data_get(s->tx_q);
uint32_t wait_ms = (uint32_t)((now - ((struct call_audio_pending*)e->data)->created_us) / 1000);
s->tx_wait_frames++; s->tx_wait_total_ms += wait_ms;
if (wait_ms > s->tx_wait_max_ms) s->tx_wait_max_ms = wait_ms;
queue_dgram_free(e); queue_entry_free(e);
}
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "audio batch TX call=%llx frames=%u target=%u bytes=%zu oldest_us=%llu first_seq=%u",
(unsigned long long)s->call_id, count, s->audio_batch.target, encoded,
(unsigned long long)age, batch.frames[0].header.seq);
if (etcp_route_send(ctx->inst, s->group_id, s->peer_node_id, wire, 1, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT) == 0) {
s->c_tx_frames += count; s->tx_batches++;
if (s->state == CALL_ST_ACTIVE && last_pts != INT64_MIN) call_video_audio_sent(s, last_pts + 20000);
} else {
s->c_tx_dropped += count;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio batch enqueue failed call=%llx frames=%u", (unsigned long long)s->call_id, count);
}
}
}
int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len,
const struct call_capture_time* timing) {
struct call_ctx* ctx = call_of(inst);
if (!ctx || !ctx->initialized || !opus || len <= 0) return -1;
if (!ctx || !ctx->initialized) return -1;
struct call_session* s = call_session_find(ctx, call_id);
if (!s || s->state != CALL_ST_ACTIVE) return -1;
if (CALL_MEDIA_HDR_SIZE + (size_t)len > 1024) return -1;
if (timing && (timing->delay_samples > CALL_OPUS_SAMPLE_RATE || (timing->pts_us != INT64_MIN
&& (timing->pts_us < 0 || timing->pts_us > INT64_MAX / 2 || !timing->epoch)))) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call media invalid capture metadata pts=%lld epoch=%llu delay=%u",
(long long)timing->pts_us, (unsigned long long)timing->epoch, timing->delay_samples);
return -1;
if (!opus || len <= 0 || (size_t)len > CALL_AUDIO_MAX_OPUS || (timing && (timing->delay_samples > CALL_OPUS_SAMPLE_RATE
|| (timing->pts_us != INT64_MIN && (timing->pts_us < 0 || timing->pts_us > INT64_MAX / 2 || !timing->epoch))))) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio frame invalid length/timing len=%d", len); return -1;
}
uint8_t buf[1024];
struct call_pkt_media* m = (struct call_pkt_media*)buf;
m->subcmd = CALL_SUBCMD_MEDIA;
m->call_id = call_id;
m->track_id = CALL_TRACK_AUDIO;
m->seq = s->tx_media_seq++;
m->ts_ms = (uint32_t)(get_time_tb() / 10);
m->capture_pts_us = timing ? timing->pts_us : INT64_MIN;
m->capture_epoch = timing ? timing->epoch : 0;
m->codec_delay_samples = timing ? timing->delay_samples : 0;
memcpy(buf + CALL_MEDIA_HDR_SIZE, opus, (size_t)len);
size_t mlen = CALL_MEDIA_HDR_SIZE + (size_t)len;
/* entry: [svc_id][media-пакет] — кладём в tx_q (переполнение → дроп старейшего) */
struct ll_entry* e = queue_entry_new(0);
if (!e) return -1;
e->dgram = u_malloc(1 + mlen);
if (!e->dgram) { queue_entry_free(e); return -1; }
e->dgram[0] = ETCP_RT_ID_CALL_AUDIO;
memcpy(e->dgram + 1, buf, mlen);
e->len = (uint16_t)(1 + mlen);
struct ll_entry* e = queue_entry_new(sizeof(struct call_audio_pending));
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio frame metadata allocation failed"); return -1; }
e->dgram = u_malloc(len);
if (!e->dgram) { queue_entry_free(e); DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio frame allocation failed len=%d", len); return -1; }
*(struct call_audio_pending*)e->data = (struct call_audio_pending){get_time_us(),
timing ? *timing : (struct call_capture_time){INT64_MIN, 0, 0}, s->tx_media_seq++};
memcpy(e->dgram, opus, len); e->len = len;
if (queue_entry_count(s->tx_q) >= CALL_MAX_TX_FRAMES) {
struct ll_entry* old = queue_data_get(s->tx_q);
if (old) {
queue_dgram_free(old); queue_entry_free(old); s->c_tx_dropped++;
if (s->c_tx_dropped == 1 || s->c_tx_dropped % 50 == 0)
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: TX overflow id=%016llx queued=%zu dropped=%u", CALL_ID,
(unsigned long long)call_id, (size_t)queue_entry_count(s->tx_q), s->c_tx_dropped);
}
queue_dgram_free(old); queue_entry_free(old); s->c_tx_dropped++;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio TX overflow call=%llx dropped=%u", (unsigned long long)call_id, s->c_tx_dropped);
}
queue_data_put(s->tx_q, e);
call_tx_flush(s);
return 0;
}
+7 -4
View File
@@ -74,7 +74,7 @@ uint8_t call_phone_status(struct UTUN_INSTANCE* inst);
// ── медиа (релей Opus-кадров, uasync-поток) ──
/* Отправить закодированный Opus-кадр пиру (оборачивает в CALL_MEDIA).
* Только ACTIVE: len=1..988, opus/timing копируются до возврата; timing=NULL — без capture PTS.
* Только ACTIVE: len=1..985, opus/timing копируются до возврата; timing=NULL — без capture PTS.
* seq/ts выставляются автоматически. 0 = принят в локальную FIFO, -1 = неверные данные/нет ACTIVE/ошибка памяти.
* Результат не подтверждает доставку: FIFO750 удаляет старейший кадр при заполнении, роутер может отказать при drain. */
int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len,
@@ -91,17 +91,20 @@ void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, v
/* ── аудио-движок (опционально, GUI-сборки) ──
* Сервисный аудио-контур (codec + джиттер-буфер + тоны) регистрируется через
* call_set_audio_ops. call.c зовёт on_media на каждый принятый медиа-кадр (с seq/ts)
* call_set_audio_ops. call.c зовёт on_media на каждый принятый медиа-кадр (с seq и временем подачи пачки в Router)
* и get_stats для события CALL_STATS. Демон без аудио (headless) — ops не ставит. */
struct call_audio_ops {
// uasync-поток: принятый Opus-кадр; opus/timing заимствованы до возврата. Декодирование определяет frontend.
// uasync: Opus/PTS заимствованы; ts_ms — время пачки, packet_start=1 только у её первого кадра.
void (*on_media)(struct UTUN_INSTANCE* inst, uint64_t call_id,
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing);
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing, int packet_start);
/* uasync-поток: метрики (buffer_ms, tempo_x100=100 для 1x, накопительные dropped/underruns, range).
* Выходы необязательны. 0 = снимок получен, -1 = нет активного DSP; реализация может потреблять события диагностики. */
int (*get_stats)(uint64_t call_id, int* buffer_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range);
/* uasync: применить opus_codec_preset (0/1/2 = 16/32/64 кбит/с) к текущему и следующим звонкам.
* Необязательный callback; 0 = применено, -1 = ошибка, прежний битрейт сохранён. */
int (*set_codec_preset)(struct UTUN_INSTANCE* inst, int preset);
};
/* Только uasync: сохранить заимствованный ops у ядра (NULL снимает регистрацию), допустимо до запуска чата.
* Объект живёт до снятия/уничтожения ядра. Stop/start чата сохраняет регистрацию, состояние звонка — нет. */
+36 -7
View File
@@ -24,7 +24,6 @@
#include <string.h>
#define CALL_AUDIO_ID "call_audio"
#define CALL_AUDIO_BITRATE 32000
#define CALL_STALL_SAMPLES (CALL_AUDIO_SAMPLE_RATE / 2) /* 500мс без PCM из jitter → глитч-тон */
#define CALL_AEC_FILTER_SAMPLES 14400 /* хвост эха 300мс @48кГц */
#define CALL_AEC_DELAY_FRAMES 2 /* desktop: минимальная задержка duplex */
@@ -39,6 +38,7 @@ static int g_ended_started = 0; /* только аудио-
static uint64_t g_call_id = 0;
static int g_gap_samples = 0; /* только аудиопоток; насыщение на пороге */
static opus_codec_encoder_t* g_encoder = NULL;
static int g_codec_preset = 1; /* Снимок настройки из uasync; защищён g_mtx. */
static opus_codec_decoder_t* g_decoder = NULL;
static struct call_jitter* g_vj = NULL;
static struct call_tones* g_tones = NULL;
@@ -102,9 +102,32 @@ static void call_audio_send(void* arg) {
if (send) call_send_media(tx->inst, tx->call_id, tx->opus, tx->len, &tx->timing);
}
static int call_audio_set_codec_preset(struct UTUN_INSTANCE* inst, int preset) {
if (preset < 0 || preset > 2) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: invalid codec preset=%d", CALL_AUDIO_ID, preset); return -1;
}
int bitrate = 16000 << preset;
pthread_mutex_lock(&g_mtx);
if (g_inst != inst) {
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: codec setting for detached instance", CALL_AUDIO_ID); return -1;
}
if (g_encoder && opus_codec_encoder_bitrate_set(g_encoder, bitrate) < 0) {
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: codec bitrate update failed preset=%d bitrate=%d", CALL_AUDIO_ID, preset, bitrate);
return -1;
}
g_codec_preset = preset;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: codec preset=%d bitrate=%d active=%d call=%016llx", CALL_AUDIO_ID,
preset, bitrate, g_active, (unsigned long long)g_call_id);
pthread_mutex_unlock(&g_mtx);
return 0;
}
static const struct call_audio_ops g_ops = {
.on_media = call_audio_on_media,
.get_stats = call_audio_get_stats,
.set_codec_preset = call_audio_set_codec_preset,
};
/* Создать AEC с текущей задержкой (объявление; определение ниже). */
@@ -128,7 +151,7 @@ int call_audio_init(struct UTUN_INSTANCE* inst) {
}
call_set_audio_ops(inst, &g_ops);
pthread_mutex_unlock(&g_mtx);
return 0;
return call_audio_set_codec_preset(inst, chat_setting_get_int(inst, "opus_codec_preset", 1));
}
void call_audio_destroy(struct UTUN_INSTANCE* inst) {
@@ -147,12 +170,12 @@ void call_audio_destroy(struct UTUN_INSTANCE* inst) {
/* ── uasync-поток: приём медиа (кладём кодированный кадр в jitter-буфер) ── */
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing) {
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing, int packet_start) {
(void)seq;
pthread_mutex_lock(&g_mtx);
if (inst == g_inst && g_active && call_id == g_call_id && g_vj && opus && len > 0) {
call_jitter_push(g_vj, opus, len, ts_ms, (int64_t)(get_time_tb() / 10), timing);
call_jitter_push(g_vj, opus, len, ts_ms, (int64_t)(get_time_tb() / 10), timing, packet_start);
}
pthread_mutex_unlock(&g_mtx);
}
@@ -190,7 +213,13 @@ static int call_audio_start_impl(uint64_t call_id, int prepared) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: encoder create failed", CALL_AUDIO_ID);
return -1;
}
opus_codec_encoder_bitrate_set(enc, CALL_AUDIO_BITRATE);
int preset = g_codec_preset, bitrate = 16000 << preset;
if (opus_codec_encoder_bitrate_set(enc, bitrate) < 0) {
opus_codec_encoder_destroy(enc);
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: encoder bitrate failed preset=%d bitrate=%d", CALL_AUDIO_ID, preset, bitrate);
return -1;
}
int codec_delay = 0;
if (opus_codec_encoder_delay(enc, &codec_delay) < 0) {
opus_codec_encoder_destroy(enc);
@@ -277,8 +306,8 @@ static int call_audio_start_impl(uint64_t call_id, int prepared) {
g_aec_reported_over = g_aec_reported_under = g_pcm_events = 0;
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: started call=0x%016llx aec=%d delay=%d compressor=%s gain=%d", CALL_AUDIO_ID,
(unsigned long long)call_id, g_aec ? 1 : 0, g_aec_delay_frames,
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: started call=0x%016llx preset=%d bitrate=%d aec=%d delay=%d compressor=%s gain=%d", CALL_AUDIO_ID,
(unsigned long long)call_id, preset, bitrate, g_aec ? 1 : 0, g_aec_delay_frames,
ac ? (audio_compressor_is_enabled(ac) ? "on" : "off") : "unavailable",
compressor_gain);
return 0;
+4 -3
View File
@@ -45,7 +45,8 @@ int call_audio_init(struct UTUN_INSTANCE* inst);
* Поздний detach прежнего inst не затрагивает нового владельца. Codec/jitter освобождаются отдельно через stop/release. */
void call_audio_destroy(struct UTUN_INSTANCE* inst);
/* GUI-поток: создать codec + jitter + tones. Старый API принимает raw PCM и использует core AEC.
/* GUI-поток: создать codec + jitter + tones. Битрейт берётся из opus_codec_preset; runtime-set обновляет его без restart.
* Старый API принимает raw PCM и использует core AEC.
* Нужен привязанный через init живой inst; без него -1. Для desktop нужен start_prepared:
* AEC и stereo reference принадлежат VoiceAudioIo. Повтор активного ID сохраняет DSP;
* смена raw/prepared для него отклоняется. До смены ID остановить оба I/O-направления. 0 = успех, -1 = ошибка. */
@@ -80,9 +81,9 @@ int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, i
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples);
/* uasync-поток (из call.c): кадр текущего inst/активного DSP передаётся в call_jitter_push;
* данные копируются, декодирование произойдёт на pull в аудио-потоке. Чужой inst/ID или отсутствие DSP — no-op. */
* данные копируются, декодирование на pull. ts_ms — время пачки; packet_start=1 только на первом кадре. Чужой inst/ID или отсутствие DSP — no-op. */
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing);
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing, int packet_start);
/* Playback worker: номер PCM-фрейма и время его вывода, epoch берётся из get_clock после start/reset.
* UI/video: get_clock возвращает 0 для существующего звонка; valid внутри снимка может быть 0.
+105
View File
@@ -0,0 +1,105 @@
#include "call_audio_batch.h"
#include "../routing_layer/etcp_router.h"
#include "../../lib/debug_config.h"
#include <string.h>
static int valid_header(const struct call_pkt_media *h) {
return h->subcmd == CALL_SUBCMD_MEDIA && h->call_id && h->track_id == CALL_TRACK_AUDIO
&& h->count && h->count <= CALL_AUDIO_BATCH_MAX && h->codec_delay_samples <= CALL_OPUS_SAMPLE_RATE;
}
static int valid_frame(const struct call_pkt_media *h, const struct call_pkt_audio_frame *f) {
return f->length && f->length <= CALL_AUDIO_MAX_OPUS && (f->capture_pts_us == INT64_MIN
|| (f->capture_pts_us >= 0 && f->capture_pts_us <= INT64_MAX / 2 && h->capture_epoch));
}
int call_audio_batch_encode(const struct call_audio_batch_view *batch, uint8_t *out, size_t capacity, size_t *length) {
if (!batch || !out || !length || !valid_header(&batch->header)) goto invalid;
size_t total = CALL_MEDIA_HDR_SIZE;
for (unsigned i = 0; i < batch->header.count; i++) {
if (!batch->frames[i].opus || !valid_frame(&batch->header, &batch->frames[i].header)) goto invalid;
total += CALL_AUDIO_FRAME_HDR_SIZE + batch->frames[i].header.length;
}
if (total > capacity || total > CALL_MEDIA_MAX_BYTES) goto invalid;
memcpy(out, &batch->header, CALL_MEDIA_HDR_SIZE);
size_t offset = CALL_MEDIA_HDR_SIZE;
for (unsigned i = 0; i < batch->header.count; i++) {
const struct call_audio_frame_view *f = &batch->frames[i];
memcpy(out + offset, &f->header, CALL_AUDIO_FRAME_HDR_SIZE); offset += CALL_AUDIO_FRAME_HDR_SIZE;
memcpy(out + offset, f->opus, f->header.length); offset += f->header.length;
}
*length = total;
return 0;
invalid:
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio batch encode invalid fields/capacity=%zu", capacity); return -1;
}
int call_audio_batch_parse(const uint8_t *wire, size_t length, struct call_audio_batch_view *out) {
if (!wire || !out || length < CALL_MEDIA_HDR_SIZE || length > CALL_MEDIA_MAX_BYTES) goto invalid;
struct call_audio_batch_view result = {0};
memcpy(&result.header, wire, CALL_MEDIA_HDR_SIZE);
if (!valid_header(&result.header)) goto invalid;
size_t offset = CALL_MEDIA_HDR_SIZE;
for (unsigned i = 0; i < result.header.count; i++) {
struct call_audio_frame_view *f = &result.frames[i];
if (length - offset < CALL_AUDIO_FRAME_HDR_SIZE) goto invalid;
memcpy(&f->header, wire + offset, CALL_AUDIO_FRAME_HDR_SIZE); offset += CALL_AUDIO_FRAME_HDR_SIZE;
if (!valid_frame(&result.header, &f->header) || f->header.length > length - offset) goto invalid;
f->opus = wire + offset; offset += f->header.length;
}
if (offset != length) goto invalid;
*out = result;
return 0;
invalid:
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio batch RX malformed bytes=%zu", length); return -1;
}
void call_audio_batch_init(struct call_audio_batch_control *c, uint64_t now) {
if (!c) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio batch reset without state"); return; }
*c = (struct call_audio_batch_control){.target = 4, .hold_us = 60000, .last_us = now};
}
int call_audio_batch_step(struct call_audio_batch_control *c, uint64_t now, const struct ETCP_ROUTER_TX_STATS *net) {
if (!c || !c->target || now < c->last_us || (net && net->last_ack_us > now)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio batch invalid control sample time=%llu", (unsigned long long)now); return -1;
}
unsigned previous = c->target;
if ((!net && (c->local_epoch || c->peer_epoch || c->feedback_received))
|| (net && (net->local_epoch != c->local_epoch || net->peer_epoch != c->peer_epoch))) {
call_audio_batch_init(c, now);
if (net) { c->local_epoch = net->local_epoch; c->peer_epoch = net->peer_epoch; }
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "audio batch reset router epoch=%llx/%llx previous=%u",
(unsigned long long)c->local_epoch, (unsigned long long)c->peer_epoch, previous);
}
c->last_us = now;
c->delay_us = net ? net->delivery_rtt_us : 0;
if (net && net->queued_age_us > c->delay_us) c->delay_us = net->queued_age_us;
if (net && net->inflight_age_us > c->delay_us) c->delay_us = net->inflight_age_us;
uint64_t interval = net && net->delivery_rtt_us > 250000 ? net->delivery_rtt_us * 2 : 500000;
uint64_t fresh_us = interval > 2000000 ? interval : 2000000;
int measured = net && net->ready && !net->no_route && !net->receive_busy && !net->probing
&& net->acked_packets && now - net->last_ack_us <= fresh_us;
if (measured) c->feedback_received = 1;
int usable = measured && net->window.valid;
const char *reason = "hold";
if (!usable) { c->high = c->low = 0; reason = "unavailable"; }
else if (c->delay_us > 100000) {
c->low = 0;
if (!c->high) { c->high = 1; c->high_since_us = now; }
if (c->target < CALL_AUDIO_BATCH_MAX && now >= c->next_change_us && now - c->high_since_us >= 200000) {
c->target++; c->next_change_us = now + interval; reason = "enqueue_ack_delay";
}
} else if (c->delay_us < 40000 && !net->queued_packets) {
c->high = 0;
if (!c->low) { c->low = 1; c->low_since_us = now; }
if (c->target > 1 && now >= c->next_change_us && now - c->low_since_us >= 2000000) {
c->target--; c->next_change_us = now + interval; c->low_since_us = now; reason = "recovered";
}
} else c->high = c->low = 0;
/* Даже при медленном снижении target быстрый канал сразу перестаёт ждать накопления. */
unsigned budget = c->delay_us > 40000 ? (unsigned)((c->delay_us - 40000) / 4 > 40000 ? 40000 : (c->delay_us - 40000) / 4) : 0;
unsigned target_wait = (c->target - 1) * 20000;
int starting = !c->feedback_received && (!net || (!net->no_route && !net->receive_busy && !net->probing));
c->hold_us = measured ? (target_wait < budget ? target_wait : budget) : (starting ? 60000 : 0);
if (c->target != previous) DEBUG_INFO(DEBUG_CATEGORY_CALL,
"audio batch target=%u->%u reason=%s delay_us=%llu router_rtt_us=%llu queued_us=%llu flight_us=%llu hold_us=%u",
previous, c->target, reason, (unsigned long long)c->delay_us, (unsigned long long)(net ? net->delivery_rtt_us : 0),
(unsigned long long)(net ? net->queued_age_us : 0), (unsigned long long)(net ? net->inflight_age_us : 0), c->hold_us);
return 0;
}
+39
View File
@@ -0,0 +1,39 @@
/* Упаковка Opus и регулятор пачек звонка. Только uasync; wire views заимствуют входной буфер.
* Контроллер использует полный Router enqueue->ACK, не вычитает базовый RTT.
* Владелец снимает статистику каждые 50мс, ограничивает ожидание пачки hold_us и отменяет свои таймеры. */
#ifndef CALL_AUDIO_BATCH_H
#define CALL_AUDIO_BATCH_H
#include "call_proto.h"
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
struct ETCP_ROUTER_TX_STATS;
struct call_audio_frame_view {
struct call_pkt_audio_frame header; /* Исходные seq/PTS/length; payload не копируется. */
const uint8_t *opus; /* Не менее length байт до конца encode/потребления view. */
};
struct call_audio_batch_view {
struct call_pkt_media header; /* Общие поля; count определяет число frames. */
struct call_audio_frame_view frames[CALL_AUDIO_BATCH_MAX];
};
struct call_audio_batch_control {
uint64_t local_epoch, peer_epoch, last_us, high_since_us, low_since_us, next_change_us;
uint64_t delay_us; /* max полного RTT и возраста Router-очередей; собственное накопление сюда не входит. */
unsigned target, hold_us; /* 1..10 кадров; старт4/60000мкс, после измерения 0..40000мкс. */
int feedback_received; /* Есть первый пригодный ACK в текущей Router epoch. */
int high, low;
};
/* Encode/parse: 0/-1. Проверяют весь пакет до изменения out/length/view; ошибки логируются.
* Encode не допускает перекрытие output с Opus; capacity >= вычисленного размера, максимум 1024. */
int call_audio_batch_encode(const struct call_audio_batch_view *batch, uint8_t *out, size_t capacity, size_t *length);
int call_audio_batch_parse(const uint8_t *wire, size_t length, struct call_audio_batch_view *out);
/* Начать с четырёх кадров и deadline60мс до первого измерения ACK. Reset нужен на каждый новый звонок. */
void call_audio_batch_init(struct call_audio_batch_control *control, uint64_t now_us);
/* Снимок NULL — канал пока отсутствует. Смена эпохи сбрасывает историю; 0/-1.
* BLOCKED/нет свежих ACK: рост запрещён. Значения порогов и причин смены пишет DEBUG_CATEGORY_CALL. */
int call_audio_batch_step(struct call_audio_batch_control *control, uint64_t now_us, const struct ETCP_ROUTER_TX_STATS *net);
#ifdef __cplusplus
}
#endif
#endif
+13 -6
View File
@@ -38,7 +38,7 @@ struct call_jitter {
CallDelay delay{jitter_decode, this, [](void* context, const soundtouch::SourceSpan& span) {
static_cast<call_jitter*>(context)->timeline.source(span);
}, this};
bool primed = false;
bool primed = false, playback_started = false;
int budget = 0, attempts = 0, error_result = 0, error_len = 0;
uint32_t decode_errors = 0;
};
@@ -100,8 +100,8 @@ struct call_jitter* call_jitter_create(call_jitter_decode_fn decode, void* arg,
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_jitter: delay processor unavailable");
j->~call_jitter(); u_free(j); return nullptr;
}
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: created capacity=%dms reserve=%d..%dms adaptive_pcm=1",
CALL_JITTER_MAX_FRAMES * CALL_JITTER_FRAME_MS, CALL_JITTER_MIN_RESERVE_MS, max_reserve_ms);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: created capacity=%dms reserve=%d..%dms startup=%dms adaptive_pcm=1",
CALL_JITTER_MAX_FRAMES * CALL_JITTER_FRAME_MS, CALL_JITTER_MIN_RESERVE_MS, max_reserve_ms, CALL_JITTER_STARTUP_MS);
return j;
}
@@ -111,7 +111,7 @@ void call_jitter_destroy(struct call_jitter* j) {
}
void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32_t sent_ms, int64_t received_ms,
const struct call_capture_time* timing) {
const struct call_capture_time* timing, int packet_start) {
if (!j || !enc || len <= 0 || len > kMaxEncoded || received_ms < 0) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: invalid frame len=%d received=%lldms", len, (long long)received_ms);
return;
@@ -122,7 +122,7 @@ void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32
(long long)j->arrival_window.last_received_ms, (long long)received_ms);
return;
}
j->arrival_window.observe_arrival(received_ms, sent_ms);
if (packet_start) j->arrival_window.observe_arrival(received_ms, sent_ms);
j->range.reserve_ms = j->arrival_window.reserve;
if (j->count == CALL_JITTER_MAX_FRAMES) {
j->head = (j->head + 1) % CALL_JITTER_MAX_FRAMES; j->count--; j->dropped++;
@@ -148,7 +148,9 @@ int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) {
}
if (!j->primed) {
// После первого полного блока остаётся сетевой запас, не скрытый запас DSP.
if (depth_samples < reserve * 48 + max_samples) {
int required = reserve * 48 + max_samples;
if (!j->playback_started) required = std::max(required, CALL_JITTER_STARTUP_MS * 48);
if (depth_samples < required) {
std::lock_guard<std::mutex> lock(j->mtx);
j->diagnostics.depth_ms = (int)depth; j->diagnostics.reserve_ms = reserve;
j->diagnostics.tempo_x100 = j->tempo_x100; j->diagnostics.waiting = 1;
@@ -162,6 +164,11 @@ int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) {
int n = (int)j->delay.pull(out, (unsigned)max_samples, count * kFrameSamples, reserve * 48);
auto state = j->delay.stats();
j->primed = n == max_samples;
if (n > 0 && !j->playback_started) {
j->playback_started = true;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: startup complete buffered_ms=%.1f target_ms=%d reserve_ms=%d pcm=%d",
depth, CALL_JITTER_STARTUP_MS, reserve, n);
}
{
std::lock_guard<std::mutex> lock(j->mtx);
j->timeline.read((unsigned)n);
+6 -3
View File
@@ -10,12 +10,13 @@ extern "C" {
/* Звонок: FIFO Opus, запас 20мс..max_reserve_ms по изменению задержки доставки за 10 секунд и догон после паузы устройства.
* push — сетевой поток, pull/decode/CallDelay — единственный аудиопоток.
* Старт/восстановление ждут запас плюс запрошенный блок; при выдаче PCM запас не резервируется повторно.
* Первый старт ждёт не менее200мс; восстановление — сетевой запас плюс блок; при выдаче PCM запас не резервируется повторно.
* destroy — после остановки всех обращений. get_stats/diagnostics/clock — любой поток,
* внутренний mutex защищает их снимки. Все операции playback (включая silence/reset)
* сериализует владелец; mutex не делает допустимыми два одновременных pull. */
#define CALL_JITTER_FRAME_MS 20
#define CALL_JITTER_MIN_RESERVE_MS 20
#define CALL_JITTER_STARTUP_MS 200 /* Минимальная накопленная речь перед первым декодированием/выводом. */
#define CALL_JITTER_MAX_FRAMES 750 /* 10с перебоя + 5с запаса на накопление и восстановление */
/* Заимствованный context, encoded/len и выход/capacity (сэмплы, до 960); вернуть 1..capacity или <=0 при ошибке.
* Callback вызывается синхронно на pull без сетевого mutex; encoded действителен только до возврата. */
@@ -44,13 +45,15 @@ void call_jitter_destroy(struct call_jitter* j);
* Абсолютное смещение часов пиров не влияет на оценку джиттера. enc (1..512 байт) и timing копируются;
* timing=NULL означает отсутствие PTS. FIFO сохраняет порядок push, сортировки/дедупликации нет.
* При заполнении удаляется старейший кадр; неверные аргументы/регрессия received_ms отклоняются с WARN. */
/* packet_start=1: учитывать arrival/sent_ms как один сетевой пакет; остальные кадры пачки не меняют jitter. */
void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32_t sent_ms, int64_t received_ms,
const struct call_capture_time* timing);
const struct call_capture_time* timing, int packet_start);
/* Только после остановки playback-операций: удалить PCM/DSP, увеличить локальную epoch,
* оставить последние ceil(reserve/20) Opus-кадров. Оценка доставки и суммарные dropped/underruns сохраняются. */
void call_jitter_reset(struct call_jitter* j);
/* out — max_samples mono int16 @48кГц; вернуть 0..max_samples, без дополнения тишиной.
* Старт/восстановление ждут reserve плюс размер запроса. Decode ограничен min(750, 8+max_samples/480) попытками.
* Первый старт ждёт max(200мс, reserve плюс размер запроса); восстановление — reserve плюс размер запроса.
* Decode ограничен min(750, 8+max_samples/480) попытками.
* NULL/неположительный размер дают 0. Вызывающий учитывает дополненную локальную тишину через silence. */
int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples);
/* Playback worker: учесть только дополненные после pull локальные сэмплы; их PTS удерживается. */
+6 -1
View File
@@ -10,6 +10,7 @@
#ifndef CALL_PRIV_H
#define CALL_PRIV_H
#include "call.h"
#include "call_audio_batch.h"
#include "../../lib/ll_queue.h"
/* IDLE — только создание; RINGING бывает у обеих ролей, WAIT_DECISION — ожидание force
@@ -33,7 +34,11 @@ struct call_session {
uint8_t connection_reported, connection_type; /* Было событие CONNECTION / последний router path.type (0..3). */
uint64_t connection_next_hop, last_media_tb; /* Кэш next hop и время последнего RX; watchdog использует traffic_timer. */
uint16_t tx_media_seq; /* Следующий sequence аудиопакета с wrap uint16; расходуется до выделения TX entry. */
struct ll_queue *tx_q; /* Собственная FIFO готовых audio packets [svc][media]; до 750, переполнение удаляет голову. */
struct call_audio_batch_control audio_batch; /* Только uasync; reset при смене Router epoch. */
void *audio_batch_timer, *audio_flush_timer; /* Наблюдение 50мс и deadline неполной пачки. */
uint64_t audio_report_us;
uint32_t tx_batches, rx_batches; /* Число Router-кодограмм, отдельно от аудиокадров. */
struct ll_queue *tx_q; /* FIFO Opus с локальными seq/PTS/created_us в entry->data; до 750, overflow удаляет голову. */
struct queue_waiter_handle send_waiter; /* Отменяемое одноразовое ожидание места в router audio send_q. */
uint32_t c_tx_frames, c_rx_frames, c_tx_dropped, c_rx_dropped; /* TX принят router / валидный RX / TX overflow / RX не ведётся. */
uint64_t tx_wait_total_ms; /* Сумма времени audio enqueue->drain, включая отказ router; сброс при отчёте stats. */
+19 -10
View File
@@ -76,21 +76,30 @@ struct call_pkt_cancel { uint8_t subcmd; uint64_t call_id; };
struct call_pkt_hangup { uint8_t subcmd; uint64_t call_id; uint8_t reason; };
struct call_pkt_end_ack { uint8_t subcmd; uint64_t call_id; };
/* Одна Router-кодограмма содержит 1..10 Opus-кадров одной capture epoch.
* После header: повторяются [call_pkt_audio_frame][opus length байт]. Без старого формата. */
struct call_pkt_media {
uint8_t subcmd; /* CALL_SUBCMD_MEDIA */
uint64_t call_id; /* ID существующего ACTIVE-звонка; peer/group проверяет call.c. */
uint8_t track_id; /* CALL_TRACK_AUDIO. */
uint16_t seq; /* Счётчик отправителя с переполнением; call_audio FIFO не сортирует по seq. */
uint32_t ts_ms; /* Монотонное время постановки в TX FIFO, миллисекунды modulo 2^32. */
int64_t capture_pts_us; /* Монотонный PTS первого входного сэмпла; INT64_MIN — часы неизвестны. */
uint64_t capture_epoch; /* Ненулевая эпоха capture при известном PTS; смена означает разрыв. */
uint32_t codec_delay_samples; /* Opus lookahead при48кГц, не более48000. */
/* Далее непустой Opus payload; call_send_media ограничивает общий media-пакет1024 байтами. */
uint8_t subcmd; /* CALL_SUBCMD_MEDIA */
uint64_t call_id; /* ACTIVE-звонок; peer/group проверяет call.c. */
uint8_t track_id; /* CALL_TRACK_AUDIO */
uint64_t capture_epoch; /* Общая эпоха источника; с известным PTS ненулевая. */
uint32_t codec_delay_samples; /* Opus lookahead при 48кГц. */
uint32_t sent_ms; /* Монотонное время подачи пачки в Router; одно измерение jitter на пачку. */
uint8_t count; /* 1..CALL_AUDIO_BATCH_MAX */
};
struct call_pkt_audio_frame {
uint16_t seq; /* Исходный номер кадра, допускает пропуски/переполнение. */
int64_t capture_pts_us; /* PTS первого сэмпла; INT64_MIN при отсутствии. */
uint16_t length; /* Непустой Opus, полностью помещается в кодограмме. */
};
#pragma pack(pop)
#define CALL_MEDIA_HDR_SIZE sizeof(struct call_pkt_media) /* 36 */
#define CALL_MEDIA_HDR_SIZE sizeof(struct call_pkt_media) /* 27 */
#define CALL_AUDIO_FRAME_HDR_SIZE sizeof(struct call_pkt_audio_frame) /* 12 */
#define CALL_AUDIO_BATCH_MAX 10u
#define CALL_MEDIA_MAX_BYTES 1024u
#define CALL_AUDIO_MAX_OPUS (CALL_MEDIA_MAX_BYTES - CALL_MEDIA_HDR_SIZE - CALL_AUDIO_FRAME_HDR_SIZE)
/* ── параметры аудио (Opus 48kHz mono, 20ms) ── */
#define CALL_OPUS_SAMPLE_RATE 48000
+2 -1
View File
@@ -307,7 +307,8 @@ struct chat_node_arg { struct UTUN_INSTANCE* inst; uint64_t node_id; };
/* Полные метрики одного соединения, вызывается в uasync. arg — u_malloc struct chat_node_arg,
* trampoline освобождает arg; inst остаётся заимствованным. CHAT_EVT_CONN_METRICS содержит UTF-8
* без завершающего NUL в длине; буфер растёт по числу линков, при OOM нет частичного события.
* без завершающего NUL в длине; включает очереди Router для пира/TX/RX-пути и transit, очереди normalizer.
* Буфер растёт по числу линков и router-сессий, при OOM нет частичного события.
* Данные ответа заимствованы только на время синхронного event callback. */
void chat_core_collect_conn_metrics_trampoline(void* arg);
+5 -1
View File
@@ -38,7 +38,7 @@ static const struct chat_setting_def g_setting_defs[] = {
{"stream_subblocks", CHAT_SETTING_INT, 8, 1, 255},
{"stream_rs_redundant", CHAT_SETTING_INT, 0, 0, 254},
{"storage_autoload", CHAT_SETTING_BOOL, 1, 0, 1},
{"opus_codec_preset", CHAT_SETTING_INT, 1, 0, 4},
{"opus_codec_preset", CHAT_SETTING_INT, 1, 0, 2},
{"radio_opus_mode", CHAT_SETTING_INT, 0, 0, 1},
{"radio_opus_bitrate", CHAT_SETTING_INT, 32, 16, 128},
{"radio_capture_stereo", CHAT_SETTING_INT, 0, 0, 1},
@@ -210,6 +210,10 @@ int chat_setting_set(struct UTUN_INSTANCE* inst, const char* name, const char* v
struct chat_setting_state* s = cs_state(inst);
if (!s) { DEBUG_ERROR(CS_ID, "chat_setting: instance is NULL"); return -2; }
int result = chat_setting_set_state(s, name, value);
if (!result && !strcmp(name, "opus_codec_preset") && inst->call_audio_ops && inst->call_audio_ops->set_codec_preset &&
inst->call_audio_ops->set_codec_preset(inst, chat_setting_get_int(inst, name, 1))) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "chat_setting: '%s' stored but codec apply failed; retry setting to reapply", name); return -3;
}
if (!result && !strcmp(name, "incoming_call_policy"))
call_set_phone_state(inst, inst->phone_state, (unsigned)chat_setting_get_int(inst, name, CALL_POLICY_PUBLISH_PHONE));
if (!result && (!strcmp(name, "local_invites_enabled") || !strcmp(name, "local_broadcasts_enabled")) &&
+2 -2
View File
@@ -52,8 +52,8 @@ int chat_setting_get_int_state(const struct chat_setting_state* s, const char*
/* ─── instance-уровень (runtime) ─── */
/* Те же коды; NULL inst — -2. Большинство потребителей читают значение при следующем обращении/старте.
* incoming_call_policy сразу обновляет телефонную политику, local_* — LAN permissions.
* -3 — значение сохранено, применение LAN не удалось; повтор set того же значения повторяет apply. */
* incoming_call_policy сразу обновляет телефонную политику, local_* — LAN permissions, opus_codec_preset — битрейт звонка.
* -3 — значение сохранено, применение не удалось; повтор set того же значения повторяет apply. */
int chat_setting_set(struct UTUN_INSTANCE* inst, const char* name, const char* value);
/* Instance-варианты чтения с теми же lifetime/default; NULL inst даёт NULL/def. */
const char* chat_setting_get(struct UTUN_INSTANCE* inst, const char* name);
+65
View File
@@ -24,6 +24,7 @@
#include "../routing_layer/topo_node.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../routing_layer/topo_group.h"
#include "../routing_layer/etcp_router.h"
#include <string.h>
struct address_profile_text { char text[80]; };
@@ -220,6 +221,65 @@ static void collect_conn_list(struct UTUN_INSTANCE* inst) {
}
/* ── Полный текстовый дамп одного соединения ── */
/* Читаемые имена основных сервисов; числовой svc выводится для каждого, включая неизвестные. */
static const char* router_service_name(uint8_t svc) {
switch (svc) {
case ETCP_RT_ID_CALL_AUDIO: return "call audio";
case ETCP_RT_ID_CALL: return "call signaling";
case ETCP_RT_ID_CALL_VIDEO: return "call video";
case ETCP_RT_ID_CALL_VIDEO_CONTROL: return "video control";
case ETCP_RT_ID_DM: return "DM";
case ETCP_RT_ID_DM_MEDIA: return "DM media";
case ETCP_RT_ID_FILE_TRANSFER: return "file transfer";
case ETCP_RT_ID_MEMBER_FILES: return "member files";
case ETCP_RT_ID_CONN_MGR: return "connection manager";
default: return "service";
}
}
/* Только чтение в uasync: сессии выбранного пира, текущего TX-пути или последнего RX-пути и его transit FIFO.
* Байты — счётчики очередей, не wire size. Ошибка форматирования возвращается сборщику для общего ERROR. */
static int append_router_metrics(struct strbuf* text, struct UTUN_INSTANCE* inst, struct ETCP_CONN* conn) {
if (strbuf_addf(text, "\n--- Router queues ---\n"
"Scope: peer / current TX hop / last RX hop\n"
"send_q: before ETCP; wait_ack: service ACK pending (may still be queued in ETCP)\n") < 0) return -1;
unsigned sessions = 0;
for (struct ll_entry* e = inst->router_conns ? inst->router_conns->head : NULL; e; e = e->next) {
struct ETCP_ROUTER_CONN* rc = (struct ETCP_ROUTER_CONN*)e;
struct ETCP_ROUTER_PATH path = etcp_router_get_path(inst, rc->group_id, rc->remote_node_id, rc->svc_id);
if (rc->remote_node_id != conn->peer_node_id && rc->recv_conn != conn && path.next_hop_node_id != conn->peer_node_id) continue;
sessions++;
if (strbuf_addf(text, "\ngroup=%016llX peer=%016llX svc=%u (%s)\n"
"ready=%u no_route=%u send_blocked=%u peer_busy=%u recv_blocked=%u transport_wait=%u\n"
"RTT=%.1fms inflight_limit=%u retrans=%u\n",
(unsigned long long)rc->group_id, (unsigned long long)rc->remote_node_id, rc->svc_id, router_service_name(rc->svc_id),
rc->peer_reset_id != 0, rc->no_route, rc->send_blocked, rc->peer_receive_blocked, rc->receive_blocked,
rc->send_waiter_q != NULL, rc->rtt / 10.0, rc->inflight_limit, rc->c_retrans_done) < 0) return -1;
struct ll_queue* queues[] = {rc->send_q, rc->inflight_q, rc->incoming_q, rc->recv_q};
const char* names[] = {"send_q", "wait_ack", "incoming_q", "recv_q"};
for (unsigned i = 0; i < 4; i++) {
if (strbuf_addf(text, "%s: %zuB/%dp%s", names[i], queues[i] ? queue_total_bytes(queues[i]) : 0,
queue_entry_count(queues[i]), i % 2 ? "\n" : " ") < 0) return -1;
}
}
if (strbuf_addf(text, "sessions: %u\n", sessions) < 0) return -1;
size_t transit_bytes = 0;
unsigned transit_packets = 0;
for (struct ll_entry* e = conn->transit_queues ? conn->transit_queues->head : NULL; e; e = e->next) {
struct TRANSIT_QUEUE* tq = (struct TRANSIT_QUEUE*)e;
size_t bytes = tq->q ? queue_total_bytes(tq->q) : 0;
unsigned packets = queue_entry_count(tq->q);
transit_bytes += bytes; transit_packets += packets;
if (strbuf_addf(text, "transit group=%016llX src=%016llX dst=%016llX: %zuB/%up\n",
(unsigned long long)tq->group_id, (unsigned long long)tq->src_node_id,
(unsigned long long)tq->dst_node_id, bytes, packets) < 0) return -1;
}
if (strbuf_addf(text, "transit total: %zuB/%up\n", transit_bytes, transit_packets) < 0) return -1;
DEBUG_TRACE(DEBUG_CATEGORY_CHAT, "connection router metrics: peer=%016llX sessions=%u transit=%zuB/%up",
(unsigned long long)conn->peer_node_id, sessions, transit_bytes, transit_packets);
return 0;
}
static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_id) {
struct strbuf text = strbuf_new();
@@ -294,6 +354,8 @@ static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_
if (conn->output_queue && strbuf_addf(&text, "out_q: %uB/%dp\n",
(unsigned)queue_total_bytes(conn->output_queue), queue_entry_count(conn->output_queue)) < 0) goto failed;
if (append_router_metrics(&text, inst, conn) < 0) goto failed;
if (strbuf_addf(&text, "\n--- ACK Debug ---\n"
"hit_inf=%u hit_sndq=%u miss=%u link_wait=%u\n",
conn->cnt_ack_hit_inf, conn->cnt_ack_hit_sndq, conn->cnt_ack_miss, conn->cnt_link_wait) < 0) goto failed;
@@ -303,8 +365,11 @@ static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_
struct PKTNORM* pn = conn->normalizer;
if (strbuf_addf(&text,
"\n--- Normalizer ---\n"
"input_q: %zuB/%dp output_q: %zuB/%dp\n"
"in: %llu pkts / %llu B out: %llu pkts / %llu B\n"
"frag_size=%u data_ptr=%u data_size=%u alloc_err=%u logic_err=%u\n",
pn->input ? queue_total_bytes(pn->input) : 0, queue_entry_count(pn->input),
pn->output ? queue_total_bytes(pn->output) : 0, queue_entry_count(pn->output),
(unsigned long long)pn->in_total_pkts, (unsigned long long)pn->in_total_bytes,
(unsigned long long)pn->out_total_pkts, (unsigned long long)pn->out_total_bytes,
pn->frag_size, pn->data_ptr, pn->data_size, pn->alloc_errors, pn->logic_errors) < 0) goto failed;
+35 -7
View File
@@ -85,6 +85,11 @@ static int router_path_append(struct ll_entry* e, uint64_t node_id, uint8_t coun
static int router_source_send(struct ETCP_CONN* conn, struct ll_entry* e) {
if (router_path_append(e, conn->instance->node_id, 0) < 0) return -1;
const struct SVC_ROUTE_HDR* h = (const struct SVC_ROUTE_HDR*)e->dgram;
DEBUG_TRACE(DEBUG_CATEGORY_TRAFFIC, "route TX conn=%s svc=%u flags=%02x seq=%u len=%u src=%016llx dst=%016llx group=%016llx epoch=%016llx",
conn->log_name, h->svc_id, h->flags, h->seq, e->len,
(unsigned long long)h->src_node_id, (unsigned long long)h->dst_node_id,
(unsigned long long)h->group_id, (unsigned long long)h->reset_id);
return etcp_send(conn, e);
}
@@ -334,6 +339,9 @@ struct ETCP_ROUTER_PATH etcp_router_get_path(struct UTUN_INSTANCE* inst, uint64_
return path;
}
/* Локальные метаданные pending; время enqueue переживает переход в inflight и повторы. */
struct router_pending { uint64_t enqueued_us; uint32_t seq; uint8_t mode; };
// Собрать и закодировать (подпись/шифрование) пакет в начале отправки.
// Возвращает финальный wire-пакет [SVC_ROUTE_HDR][payload][sig?] (u_malloc).
static int router_build_packet(struct ETCP_ROUTER_CONN* rconn, struct ll_entry* pending,
@@ -346,13 +354,13 @@ static int router_build_packet(struct ETCP_ROUTER_CONN* rconn, struct ll_entry*
hdr->group_id = rconn->group_id;
hdr->dst_node_id = rconn->remote_node_id;
hdr->src_node_id = rconn->inst->node_id;
memcpy(&hdr->seq, pending->data, 4);
hdr->seq = ((struct router_pending*)pending->data)->seq;
hdr->svc_id = rconn->svc_id;
hdr->reset_id = rconn->reset_id;
hdr->peer_reset_id = rconn->peer_reset_id;
hdr->timestamp = get_current_timestamp();
memcpy(base + SVC_ROUTE_HDR_SIZE, pending->dgram, pending->len);
int ret = route_crypto_encode(rconn->inst, rconn->remote_node_id, base, len, pending->data[4], out, out_len);
int ret = route_crypto_encode(rconn->inst, rconn->remote_node_id, base, len, ((struct router_pending*)pending->data)->mode, out, out_len);
u_free(base);
return ret;
}
@@ -528,7 +536,7 @@ static int router_conn_reset(struct ETCP_ROUTER_CONN* rconn) {
rconn->peer_receive_blocked = 0;
rconn->recv_conn = NULL;
rconn->rtt = 0; rconn->rtt_jitter = 0;
rconn->rtt = 0; rconn->rtt_jitter = 0; rconn->delivery_rtt_us = 0;
rconn->max_inflight = ROUTER_MAX_INFLIGHT;
rconn->inflight_limit = ROUTER_MAX_INFLIGHT;
rconn->minrtt = MINRTT_DEFAULT_TB;
@@ -646,7 +654,8 @@ static void router_send_drain_cb(struct ll_queue* q, void* arg) {
free_entry(e); u_free(inf->dgram); u_free(inf); return;
}
memcpy(e->dgram, inf->dgram, len); e->len = len;
memcpy(&inf->seq, pending->data, 4); inf->ll.size = 4; inf->ll.len = pending->len;
inf->seq = ((struct router_pending*)pending->data)->seq; inf->ll.size = 4; inf->ll.len = pending->len;
inf->enqueued_us = ((struct router_pending*)pending->data)->enqueued_us;
inf->dgram_len = len; inf->last_sent_tb = get_time_tb(); inf->send_count = 1;
uint32_t seq = inf->seq;
queue_data_put_with_index(rconn->inflight_q, &inf->ll);
@@ -814,12 +823,18 @@ static int router_enqueue_send(struct ETCP_ROUTER_CONN* rconn, const uint8_t* pa
return -1;
}
if (!force && queue_entry_count(rconn->send_q) >= ROUTER_MAX_SEND_Q_PACKETS) return -1;
struct ll_entry* e = queue_entry_new(5);
struct ll_entry* e = queue_entry_new(sizeof(struct router_pending));
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router enqueue: allocation failed"); return -1; }
e->dgram = u_malloc(len);
if (!e->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router enqueue: payload allocation failed"); queue_entry_free(e); return -1; }
memcpy(e->data, &rconn->tx_seq, 4); e->data[4] = mode;
*(struct router_pending*)e->data = (struct router_pending){get_time_us(), rconn->tx_seq, (uint8_t)mode};
memcpy(e->dgram, payload, len); e->len = len;
DEBUG_TRACE(DEBUG_CATEGORY_TRAFFIC, "route enqueue peer=%016llx group=%016llx epoch=%016llx svc=%u seq=%u bytes=%zu "
"queued=%d inflight=%d limit=%u ready=%u no_route=%u peer_busy=%u waiter=%u",
(unsigned long long)rconn->remote_node_id, (unsigned long long)rconn->group_id,
(unsigned long long)rconn->reset_id, rconn->svc_id, rconn->tx_seq, len,
queue_entry_count(rconn->send_q), queue_entry_count(rconn->inflight_q), rconn->inflight_limit,
rconn->peer_reset_id != 0, rconn->no_route, rconn->peer_receive_blocked, rconn->send_waiter_q != NULL);
rconn->tx_seq++;
queue_data_put(rconn->send_q, e);
router_send_kick(rconn);
@@ -1205,7 +1220,12 @@ static void router_handle_ack(struct UTUN_INSTANCE* inst, struct ETCP_ROUTER_CON
while (rconn->inflight_q->head) {
struct ROUTER_INFLIGHT* inf = (struct ROUTER_INFLIGHT*)rconn->inflight_q->head;
if ((int32_t)(inf->seq - seq) >= 0) break;
route_tx_meter_ack(&rconn->delivery, get_time_us(), inf->ll.len);
uint64_t now_us = get_time_us(), elapsed = now_us - inf->enqueued_us;
rconn->delivery_rtt_us = rconn->delivery.packets ? rconn->delivery_rtt_us - rconn->delivery_rtt_us / 8 + elapsed / 8 : elapsed;
DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "router delivery svc=%u seq=%u enqueue_ack_us=%llu average_us=%llu attempts=%u",
rconn->svc_id, inf->seq, (unsigned long long)elapsed,
(unsigned long long)rconn->delivery_rtt_us, inf->send_count);
route_tx_meter_ack(&rconn->delivery, now_us, inf->ll.len);
queue_remove_data(rconn->inflight_q, &inf->ll);
u_free(inf->dgram); u_free(inf);
}
@@ -1341,6 +1361,9 @@ static void etcp_router_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry)
struct SVC_ROUTE_HDR* h = (struct SVC_ROUTE_HDR*)entry->dgram;
size_t body = router_path_check(conn, entry);
if (!body) { free_entry(entry); return; }
DEBUG_TRACE(DEBUG_CATEGORY_TRAFFIC, "route RX conn=%s svc=%u flags=%02x seq=%u len=%u src=%016llx dst=%016llx",
conn->log_name, h->svc_id, h->flags, h->seq, entry->len,
(unsigned long long)h->src_node_id, (unsigned long long)h->dst_node_id);
if (h->dst_node_id != inst->node_id) {
if (router_path_append(entry, inst->node_id, entry->dgram[entry->len - 1]) < 0) {
inst->router_path_errors++; free_entry(entry); return;
@@ -1572,12 +1595,17 @@ int etcp_router_tx_stats(struct UTUN_INSTANCE *inst, uint64_t group, uint64_t pe
.queued_bytes = rconn->send_q->total_bytes, .inflight_bytes = rconn->inflight_q->total_bytes,
.queued_packets = rconn->send_q->count, .inflight_packets = rconn->inflight_q->count,
.acked_bytes = rconn->delivery.bytes, .acked_packets = rconn->delivery.packets,
.delivery_rtt_us = rconn->delivery_rtt_us, .last_ack_us = rconn->delivery.packets ? rconn->delivery.last_us : 0,
.rtt_us = (uint32_t)rconn->rtt * 100,
.baseline_rtt_us = rconn->minrtt_window_count ? (uint32_t)rconn->minrtt * 100 : 0,
.ready = rconn->peer_reset_id != 0, .no_route = rconn->no_route,
.receive_busy = rconn->peer_receive_blocked, .probing = rconn->minrtt_probe
};
if (route_tx_meter_window(&rconn->delivery, now_us, &result.window) < 0) return -1;
if (rconn->send_q->head)
result.queued_age_us = now_us - ((struct router_pending*)rconn->send_q->head->data)->enqueued_us;
if (rconn->inflight_q->head)
result.inflight_age_us = now_us - ((struct ROUTER_INFLIGHT*)rconn->inflight_q->head)->enqueued_us;
*out = result;
return 0;
}
+4
View File
@@ -179,6 +179,7 @@ struct ETCP_ROUTER_CONN {
uint32_t c_oob_dropped; // дропнуто out-of-bounds seq
uint32_t c_stale_ack; // ACK вне диапазона или без продвижения seq, включая допустимый повтор RECEIVE_BUSY
uint32_t c_sign_fail; // signature verification failures
uint64_t delivery_rtt_us; // EWMA enqueue->ACK (1/8), включает send_q/нижние очереди/обратный путь.
struct route_tx_meter delivery; // первые подтверждения исходного payload, без wire overhead
};
@@ -187,6 +188,8 @@ struct ETCP_ROUTER_CONN {
struct ETCP_ROUTER_TX_STATS {
uint64_t local_epoch, peer_epoch, queued_bytes, inflight_bytes, acked_bytes, acked_packets;
uint32_t queued_packets, inflight_packets;
uint64_t delivery_rtt_us, last_ack_us; // Полный RTT enqueue->ACK и монотонное время последнего продвижения ACK.
uint64_t queued_age_us, inflight_age_us; // Возраст головы от исходного enqueue; 0 при пустой очереди.
struct route_tx_window window;
uint32_t rtt_us, baseline_rtt_us;
int ready, no_route, receive_busy, probing;
@@ -225,6 +228,7 @@ int etcp_router_tx_stats(struct UTUN_INSTANCE *inst, uint64_t group, uint64_t pe
struct ROUTER_INFLIGHT {
struct ll_entry ll; // индекс по seq (4 байта, offset 0)
uint32_t seq;
uint64_t enqueued_us; // Исходное время постановки в send_q, неизменно при повторах.
uint64_t last_sent_tb; // время последней отправки (0.1ms)
uint8_t send_count; // успешные отправки, включая первую; диагностический циклический счётчик
uint8_t* dgram; // неизменяемое тело [SVC_ROUTE_HDR][payload][sig?], без visited_ids/count
+35 -1
View File
@@ -932,7 +932,8 @@ static void input_queue_cb(struct ll_queue* q, void* arg) {
// memory_pool_free(etcp->io_pool, in_pkt);// перемещаем из io_pool в inflight_pool
queue_entry_free(&in_pkt->ll);
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "[%s] TX input -> inflight (seq=%u, len=%u); Qlen: in=%d snd=%d wait=%d", etcp->log_name, p->seq, p->ll.len, etcp->input_queue->count, etcp->input_send_q->count, etcp->input_wait_ack->count);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[%s] TX input -> inflight seq=%u len=%u input=%d send=%d wait=%d",
etcp->log_name, p->seq, p->ll.len, etcp->input_queue->count, etcp->input_send_q->count, etcp->input_wait_ack->count);
int len=p->ll.len;// сохраним len
// Add to send queue
@@ -1076,6 +1077,38 @@ static void etcp_link_ready_callback(struct ETCP_CONN* etcp) {
etcp_conn_wake_sender(etcp);
}
/* Снимок после прохода отправителя: причины могут пересекаться, full относится к голове очереди.
* Только BLE и включённая debug-категория; не вызывает selection/BBR и не меняет состояние очередей. */
static void trace_ble_tx_wait(struct ETCP_CONN* conn) {
if (!debug_should_output(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_DEBUG) || conn->close_requested || conn->state == 2) return;
struct PKTNORM* pn = conn->normalizer;
if (!conn->input_send_q->count && !conn->input_queue->count &&
(!pn || (!queue_entry_count(pn->input) && !pn->pending && !pn->data_ptr))) return;
struct INFLIGHT_PACKET* head = (struct INFLIGHT_PACKET*)conn->input_send_q->head;
unsigned input_len = conn->input_queue->head ? conn->input_queue->head->len : 0;
uint64_t now = get_time_tb();
for (struct ETCP_LINK* link = conn->links; link; link = link->next) {
struct packet_link* channel = etcp_link_io_get(link);
if (!channel || strcmp(packet_link_name(channel), "BLE")) continue;
unsigned bytes = head ? head->ll.len : input_len;
int cwnd_full = bytes && (bytes > link->inflight_lim_bytes || link->inflight_bytes > link->inflight_lim_bytes - bytes);
int attempts_full = conn->active_attempts >= 2 * MAX_INFLIGHT_SIZE || queue_entry_count(link->attempts) >= MAX_INFLIGHT_SIZE;
int input_full = input_len && (input_len > conn->optimal_inflight || conn->unacked_bytes > conn->optimal_inflight - input_len);
uint64_t pacing_tb = link->shaper_load_time_tb >= now + SHAPER_BURST_DELAY_TB ?
link->shaper_load_time_tb - now - SHAPER_BURST_DELAY_TB + 1 : 0;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[%s] TX gate path=%u head_seq=%u bytes=%u retry=%u io=%d up=%u init=%u "
"cwnd_full=%d attempts_full=%d pacing=%d pacing_us=%llu rate_Bps=%u flight=%u/%u "
"input_full=%d unacked=%u/%u peer_full=%d input=%d send=%d norm=%d pending=%u buffered=%u token=%d",
conn->log_name, link->local_link_id, head ? head->seq : 0, bytes, head && head->last_attempt_id != 0,
etcp_link_io_ready(link), link->link_status, link->initialized, cwnd_full, attempts_full,
link->shaper_timer != NULL, (unsigned long long)(pacing_tb * 100), link->bbr_pacing_rate,
link->inflight_bytes, link->inflight_lim_bytes, input_full, conn->unacked_bytes, conn->optimal_inflight,
(uint32_t)(conn->next_tx_id - conn->rx_ack_till) > MAX_INFLIGHT_SIZE,
conn->input_queue->count, conn->input_send_q->count, pn ? queue_entry_count(pn->input) : 0,
pn && pn->pending ? pn->pending->len - pn->pending_in_ptr : 0, pn ? pn->data_ptr : 0, conn->tx_token != NULL);
}
}
// Process packets in send queue and transmit them
static void etcp_conn_process_send_queue(struct ETCP_CONN* etcp) {// вызываем когда есть элемент в send_q или надо отправить ack
if (etcp->tx_running || etcp->close_requested || etcp->state == 2) return;
@@ -1088,6 +1121,7 @@ static void etcp_conn_process_send_queue(struct ETCP_CONN* etcp) {// вызыв
}
etcp->callbacks_running--;
etcp->tx_running = 0;
trace_ble_tx_wait(etcp);
}
/* Один запуск обслуживает накопленные события; заблокированный DATA ждёт ACK/RTO/shaper, а не следующий poll. */
+4
View File
@@ -245,6 +245,10 @@ int etcp_send(struct ETCP_CONN* conn, struct ll_entry* entry) {
(void*)conn, q->count, q->size_limit, q->hash_size);
return -1;
}
if (entry->dgram && entry->len) {
DEBUG_TRACE(DEBUG_CATEGORY_TRAFFIC, "enqueue id=0x%02x conn=%s len=%u pending=%d",
entry->dgram[0], conn->log_name, entry->len, q->count);
}
return queue_data_put(conn->send_input_q, entry);
}
+6 -1
View File
@@ -159,7 +159,12 @@ static void changed(struct packet_link* channel, void* arg) {
goto done;
}
}
if (state.tx_ready) etcp_conn_wake_sender(conn);
if (state.tx_ready) {
if (!io->native) DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[%s] carrier ready path=%u driver=%s input=%d send=%d token=%d",
conn->log_name, owner->local_link_id, packet_link_name(channel), queue_entry_count(conn->input_queue),
queue_entry_count(conn->input_send_q), conn->tx_token != NULL);
etcp_conn_wake_sender(conn);
}
done:
etcp_link_release(owner); conn->callbacks_running--;
}
+2 -1
View File
@@ -217,7 +217,8 @@ static int pn_send_to_etcp(struct PKTNORM* pn) {
pn->data = NULL;
pn->data_ptr = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "pn->etcp: size=%d memlen=%d frag_size=%d", frag->ll.len, frag->ll.memlen, pn->frag_size);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[%s] normalizer TX fragment bytes=%u capacity=%u frag_size=%u input=%d",
pn->etcp->log_name, frag->ll.len, frag->ll.memlen, pn->frag_size, queue_entry_count(pn->input));
if (debug_should_output(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_DUMP)) log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_DUMP, "NORM->ETCP", frag->ll.dgram, frag->ll.len);
if (queue_data_put(pn->etcp->input_queue, (struct ll_entry*)frag) < 0) {
+4
View File
@@ -13,6 +13,7 @@ check_PROGRAMS = \
test_video_no_ffmpeg \
test_video_plane \
test_route_tx_stats \
test_call_audio_batch \
test_call_video_rate \
test_call_video_playout \
test_call_video_port \
@@ -1070,6 +1071,9 @@ test_call_SOURCES = test_call.c
test_route_tx_stats_SOURCES = test_route_tx_stats.c
test_route_tx_stats_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_call_audio_batch_SOURCES = test_call_audio_batch.c
test_call_audio_batch_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_call_video_rate_SOURCES = test_call_video_rate.c
test_call_video_rate_LDADD = $(LIBUTUN) $(COMMON_LIBS)
+55 -4
View File
@@ -69,6 +69,7 @@ enum call_phase {
P_WAIT_ACCEPTED, /* обе ACTIVE */
P_SEND_MEDIA, /* A: call_send_media (тестовый Opus-кадр) */
P_WAIT_MEDIA, /* B: media_recv_cb получил кадр */
P_BATCH_SEND, P_BATCH_WAIT, P_PARTIAL_WAIT,
P_VIDEO_CONFIG, P_VIDEO_SEND, P_VIDEO_HOLD, P_VIDEO_BUSY, P_VIDEO_RESUME, P_VIDEO_RANGE_WAIT, P_VIDEO_SWITCH, P_VIDEO_SWITCH_WAIT,
P_VIDEO_SHRINK_WAIT, P_VIDEO_PORT_OPEN, P_VIDEO_PORT_RECEIVE, P_VIDEO_PORT_SEND, P_VIDEO_PORT_ACK,
P_VIDEO_DECODE_OK, P_VIDEO_DECODE_REJECT,
@@ -134,12 +135,14 @@ struct media_evt {
};
static struct media_evt g_media[2];
static struct call_capture_time g_capture_timing;
static unsigned g_audio_packets;
static const struct call_capture_time sent_timing = {1234567890123ll, 19, 312};
static void audio_recv_cb(struct UTUN_INSTANCE* inst, uint64_t call_id, uint16_t seq, uint32_t ts,
const uint8_t* opus, int len, const struct call_capture_time* timing) {
(void)inst; (void)call_id; (void)seq; (void)ts; (void)opus; (void)len;
const uint8_t* opus, int len, const struct call_capture_time* timing, int packet_start) {
(void)inst; (void)call_id; (void)seq; (void)ts; (void)opus; (void)len; (void)packet_start;
g_capture_timing = *timing;
g_audio_packets += packet_start != 0;
}
static const struct call_audio_ops audio_ops = {.on_media = audio_recv_cb};
@@ -586,6 +589,48 @@ static void call_tick(void* arg) {
|| g_capture_timing.epoch != sent_timing.epoch || g_capture_timing.delay_samples != sent_timing.delay_samples) {
fprintf(stderr, "B: capture timing changed in transit\n"); t->result = 2; break;
}
t->phase = P_BATCH_SEND;
}
break;
case P_BATCH_SEND: {
struct call_session *cs = call_session_find(A->call, t->call_id);
if (cs->audio_batch_timer) uasync_cancel_timeout(t->ua, cs->audio_batch_timer);
cs->audio_batch_timer = NULL; cs->audio_batch.target = 10; cs->audio_batch.hold_us = 40000;
uint8_t opus[16] = {0};
for (unsigned i = 1; i <= 10; i++) {
struct call_capture_time timing = sent_timing; timing.pts_us += i * 20000;
if (call_send_media(A, t->call_id, opus, sizeof(opus), &timing)) { t->result = 2; break; }
}
if (cs->tx_batches != 2 || cs->c_tx_frames != 11 || cs->audio_flush_timer) {
fprintf(stderr, "full batch was not sent once/cancelled timer\n"); t->result = 2;
}
t->phase = P_BATCH_WAIT; break;
}
case P_BATCH_WAIT:
if (g_media[IDX_B].frames >= 11) {
if (g_media[IDX_B].frames != 11 || g_audio_packets != 2 || g_capture_timing.pts_us != sent_timing.pts_us + 200000) {
fprintf(stderr, "batch frames/timing not preserved\n"); t->result = 2; break;
}
struct call_session *cs = call_session_find(A->call, t->call_id);
uint8_t opus[16] = {0};
for (unsigned i = 11; i <= 13; i++) {
struct call_capture_time timing = sent_timing; timing.pts_us += i * 20000;
if (call_send_media(A, t->call_id, opus, sizeof(opus), &timing)) { t->result = 2; break; }
}
if (cs->tx_batches != 2 || !cs->audio_flush_timer || cs->tx_q->count != 3) {
fprintf(stderr, "partial batch did not wait for deadline\n"); t->result = 2;
}
t->phase = P_PARTIAL_WAIT;
}
break;
case P_PARTIAL_WAIT:
if (g_media[IDX_B].frames >= 14) {
struct call_session *cs = call_session_find(A->call, t->call_id);
if (g_media[IDX_B].frames != 14 || g_audio_packets != 3 || cs->tx_batches != 3 || cs->audio_flush_timer
|| g_capture_timing.pts_us != sent_timing.pts_us + 260000) {
fprintf(stderr, "partial batch deadline delivery failed\n"); t->result = 2; break;
}
cs->audio_batch.target = 1; cs->audio_batch.hold_us = 0;
t->phase = P_VIDEO_CONFIG;
}
break;
@@ -642,7 +687,7 @@ static void call_tick(void* arg) {
}
case P_VIDEO_BUSY:
if (g_video_frames) { fprintf(stderr, "video bypassed receive backpressure\n"); t->result = 2; break; }
if (++t->phase_ticks < 10 || g_media[IDX_B].frames != 2) break;
if (++t->phase_ticks < 10 || g_media[IDX_B].frames != 15) break;
g_video_ready = 1; call_video_resume_receive(B, t->call_id); t->phase = P_VIDEO_RESUME; break;
case P_VIDEO_RESUME:
if (!g_video_frames || g_video_preroll != 6 || !g_rate_commands) break;
@@ -779,9 +824,15 @@ static void call_tick(void* arg) {
call_video_port_request_key(t->call_id);
t->previous_call_id = t->call_id;
g_drop_acks[IDX_B] = 2;
struct call_session *pending_audio = call_session_find(B->call, t->call_id);
pending_audio->audio_batch.target = 10; pending_audio->audio_batch.hold_us = 40000;
uint8_t unfinished_opus[2] = {1, 2};
if (call_send_media(B, t->call_id, unfinished_opus, sizeof(unfinished_opus), &sent_timing)
|| !pending_audio->audio_flush_timer) { t->result = 2; break; }
call_hangup(B, t->call_id);
struct call_session* ended = call_session_find(B->call, t->call_id);
if (!ended || ended->tx_q || ended->video || !ended->end_subcmd || !ended->teardown_timer) {
if (!ended || ended->tx_q || ended->video || ended->audio_flush_timer || ended->audio_batch_timer
|| !ended->end_subcmd || !ended->teardown_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "ended call retained media or lost final command"); t->result = 2; break;
}
t->phase = P_VIDEO_PORT_CANCELLED; break;
+100
View File
@@ -0,0 +1,100 @@
/* Wire: границы/атомарный отказ. Регулятор: полный RTT, очередь без ACK, гистерезис и смена эпохи. */
#include "../src/call/call_audio_batch.h"
#include "../src/routing_layer/etcp_router.h"
#include "../lib/debug_config.h"
#include <stdlib.h>
#include <string.h>
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio batch test line=%d: %s", __LINE__, #x); exit(1); } } while (0)
static void wire_test(void) {
uint8_t opus[57], wire[CALL_MEDIA_MAX_BYTES], saved[CALL_MEDIA_MAX_BYTES];
memset(opus, 0xa5, sizeof(opus));
struct call_audio_batch_view b = {.header = {.subcmd = CALL_SUBCMD_MEDIA, .call_id = 1, .track_id = CALL_TRACK_AUDIO,
.capture_epoch = 17, .codec_delay_samples = 312, .sent_ms = UINT32_MAX}};
size_t length = 0;
for (unsigned n = 1; n <= 10; n++) {
b.header.count = n;
b.frames[n - 1] = (struct call_audio_frame_view){.header = {(uint16_t)(65532 + n), 1000000 + n * 20000, 37}, .opus = opus};
CHECK(call_audio_batch_encode(&b, wire, sizeof(wire), &length) == 0 && length == 27 + n * 49);
struct call_audio_batch_view out;
CHECK(call_audio_batch_parse(wire, length, &out) == 0 && out.header.count == n && out.header.sent_ms == UINT32_MAX);
for (unsigned i = 0; i < n; i++) CHECK(out.frames[i].header.seq == b.frames[i].header.seq
&& out.frames[i].header.capture_pts_us == b.frames[i].header.capture_pts_us
&& out.frames[i].header.length == 37 && !memcmp(out.frames[i].opus, opus, 37));
}
struct call_audio_batch_view out, before;
memset(&out, 0x77, sizeof(out)); before = out;
for (size_t n = 0; n < length; n++) {
CHECK(call_audio_batch_parse(wire, n, &out) == -1 && !memcmp(&out, &before, sizeof(out)));
}
CHECK(call_audio_batch_parse(wire, length + 1, &out) == -1);
memcpy(saved, wire, sizeof(wire));
wire[CALL_MEDIA_HDR_SIZE - 1] = 11;
CHECK(call_audio_batch_parse(wire, length, &out) == -1);
memcpy(wire, saved, sizeof(wire));
struct call_pkt_audio_frame invalid = {1, 0, UINT16_MAX};
memcpy(wire + CALL_MEDIA_HDR_SIZE, &invalid, sizeof(invalid));
CHECK(call_audio_batch_parse(wire, length, &out) == -1);
memset(wire, 0x55, sizeof(wire)); memcpy(saved, wire, sizeof(wire));
CHECK(call_audio_batch_encode(&b, wire, length - 1, &length) == -1 && !memcmp(wire, saved, sizeof(wire)));
b.header.count = 1; b.header.capture_epoch = 0;
CHECK(call_audio_batch_encode(&b, wire, sizeof(wire), &length) == -1);
b.frames[0].header.capture_pts_us = INT64_MIN;
CHECK(call_audio_batch_encode(&b, wire, sizeof(wire), &length) == 0);
CHECK(call_audio_batch_parse(wire, length, &out) == 0 && out.frames[0].header.capture_pts_us == INT64_MIN);
}
static void step(struct call_audio_batch_control *c, struct ETCP_ROUTER_TX_STATS *n, uint64_t t) {
n->last_ack_us = t; CHECK(call_audio_batch_step(c, t, n) == 0);
}
static void control_test(void) {
struct call_audio_batch_control c;
struct ETCP_ROUTER_TX_STATS n = {.local_epoch = 1, .peer_epoch = 2, .ready = 1, .acked_packets = 1,
.delivery_rtt_us = 20000, .window = {.valid = 1}};
call_audio_batch_init(&c, 0);
CHECK(c.target == 4 && c.hold_us == 60000);
CHECK(call_audio_batch_step(&c, 0, NULL) == 0 && c.target == 4 && c.hold_us == 60000);
n.ready = 0; n.acked_packets = 0; n.window.valid = 0;
step(&c, &n, 50000); CHECK(c.target == 4 && c.hold_us == 60000);
n.ready = 1; n.acked_packets = 1;
step(&c, &n, 100000); CHECK(c.target == 4 && c.hold_us == 0); // Первый ACK, не ждём окна500мс.
n.window.valid = 1;
for (unsigned t = 150000; t < 2000000; t += 50000) step(&c, &n, t);
CHECK(c.target == 4 && c.hold_us == 0);
/* Изолированная задержка не включает упаковку. */
n.delivery_rtt_us = 150000; step(&c, &n, 2000000); step(&c, &n, 2050000);
n.delivery_rtt_us = 20000; step(&c, &n, 2100000); CHECK(c.target == 4);
/* Даже без queued/inflight высокий обычный RTT включает пачки. */
n.delivery_rtt_us = 150000;
for (unsigned t = 2150000; t <= 2350000; t += 50000) step(&c, &n, t);
CHECK(c.target == 5 && c.hold_us == 27500);
for (unsigned t = 2400000; t <= 2850000; t += 50000) step(&c, &n, t);
CHECK(c.target == 6 && c.hold_us == 27500);
n.delivery_rtt_us = 300000;
for (unsigned t = 2900000; t <= 9000000; t += 50000) step(&c, &n, t);
CHECK(c.target == 10 && c.hold_us == 40000);
n.delivery_rtt_us = 10000; step(&c, &n, 9050000);
CHECK(c.target == 10 && c.hold_us == 0);
for (unsigned t = 9100000; t <= 11050000; t += 50000) step(&c, &n, t);
CHECK(c.target == 9);
n.peer_epoch = 3; step(&c, &n, 11100000); CHECK(c.target == 4);
n.inflight_age_us = 150000;
for (unsigned t = 11150000; t <= 11350000; t += 50000) step(&c, &n, t);
CHECK(c.target == 5);
n.receive_busy = 1;
for (unsigned t = 11400000; t <= 13000000; t += 50000) step(&c, &n, t);
CHECK(c.target == 5 && c.hold_us == 0);
n.receive_busy = 0; CHECK(call_audio_batch_step(&c, 20000000, &n) == 0 && c.target == 5 && c.hold_us == 0);
CHECK(call_audio_batch_step(&c, 19999999, &n) == -1);
CHECK(call_audio_batch_step(&c, 20000001, NULL) == 0 && c.target == 4 && c.hold_us == 60000);
n.inflight_age_us = 0;
for (unsigned t = 20100000; t <= 27000000; t += 50000) step(&c, &n, t);
CHECK(c.target == 1 && c.hold_us == 0);
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
wire_test(); control_test();
debug_set_level(DEBUG_LEVEL_INFO);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS audio batch: 1..10, partial/trailing/invalid wire, full RTT, hysteresis, deadline budget, epochs/stall");
return 0;
}
+56
View File
@@ -4,6 +4,8 @@
#include "../src/routing_layer/topo_node_sqlite.h"
#include "../src/transport_layer/etcp.h"
#include "../src/transport_layer/etcp_connections.h"
#include "../src/transport_layer/pkt_normalizer.h"
#include "../src/routing_layer/etcp_router.h"
#include "../lib/strbuf.h"
#include <assert.h>
@@ -20,6 +22,7 @@ static uint8_t detail[CHAT_MEMBER_DETAIL_MAX_SIZE];
static size_t detail_size;
static unsigned text_events;
static int expect_large_text;
static int expect_router_metrics;
static int matches(const char* sql) { return target && !strncmp(sql, target, strlen(target)); }
static int take(enum fault point, const char* sql, int index) {
@@ -86,6 +89,16 @@ static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int
if (type == CHAT_EVT_STATUS_REFRESH || type == CHAT_EVT_CONN_METRICS) {
assert(size > 0);
if (expect_large_text) assert(size > 8192 && memmem(data, (size_t)size, "LINK#299", 8));
if (type == CHAT_EVT_CONN_METRICS && expect_router_metrics) {
const char* expected[] = {"--- Router queues ---", "peer=0000000000000009 svc=63 (call audio)",
"peer=000000000000000B svc=52 (DM)", "sessions: 2\n", "send_q: 202B/2p wait_ack: 303B/1p",
"incoming_q: 44B/1p recv_q: 55B/1p", "send_q: 0B/0p wait_ack: 0B/0p",
"transit total: 666B/2p", "input_q: 777B/1p output_q: 0B/0p"};
for (unsigned i = 0; i < sizeof(expected) / sizeof(expected[0]); i++)
assert(memmem(data, (size_t)size, expected[i], strlen(expected[i])));
const char* excluded = "peer=000000000000000C svc=";
assert(!memmem(data, (size_t)size, excluded, strlen(excluded)));
}
text_events++; return;
}
if (type == CHAT_EVT_MEMBER_DETAIL) {
@@ -232,6 +245,16 @@ static void node_names(struct UTUN_INSTANCE* inst) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS node names:4 SQL faults/retries,cleared failure output and zero-size guard");
}
/* Очереди без callbacks: снимок обязан сохранить элементы и байтовые счётчики. */
static struct ll_queue* metrics_queue(struct UTUN_INSTANCE* inst, unsigned count, uint16_t bytes) {
struct ll_queue* q = queue_new(inst->ua, 0, 0, 0, "metrics_test"); assert(q);
for (unsigned i = 0; i < count; i++) {
struct ll_entry* e = ll_alloc_lldgram(bytes); assert(e); e->len = bytes;
assert(!queue_data_put(q, e));
}
return q;
}
static void diagnostics(struct UTUN_INSTANCE* inst) {
struct ETCP_CONN connection = {.instance = inst, .peer_node_id = 9, .unacked_bytes = UINT32_MAX};
struct ETCP_LINK* links = u_calloc(300, sizeof(*links)); assert(links); connection.links = links;
@@ -242,17 +265,50 @@ static void diagnostics(struct UTUN_INSTANCE* inst) {
struct ll_entry* entry = queue_entry_new(sizeof(struct conn_queue_entry)); assert(entry);
struct conn_queue_entry* item = (struct conn_queue_entry*)entry->data; item->peer_node_id = 9; item->conn = &connection;
assert(!queue_data_put_with_index(inst->connections, entry)); expect_large_text = 1; unsigned before = text_events;
struct ll_queue* saved_router = inst->router_conns;
inst->router_conns = metrics_queue(inst, 0, 0);
struct ETCP_ROUTER_CONN* sessions[3];
for (unsigned i = 0; i < 3; i++) {
sessions[i] = (struct ETCP_ROUTER_CONN*)queue_entry_new(sizeof(*sessions[i]) - sizeof(struct ll_entry)); assert(sessions[i]);
sessions[i]->inst = inst; sessions[i]->remote_node_id = i ? 10 + i : 9;
sessions[i]->svc_id = i ? ETCP_RT_ID_DM : ETCP_RT_ID_CALL_AUDIO;
assert(!queue_data_put(inst->router_conns, &sessions[i]->ll));
}
sessions[1]->recv_conn = &connection;
struct ll_queue* queues[] = {metrics_queue(inst, 2, 101), metrics_queue(inst, 1, 303), metrics_queue(inst, 1, 44),
metrics_queue(inst, 1, 55), metrics_queue(inst, 2, 333), metrics_queue(inst, 1, 777)};
sessions[0]->send_q = queues[0]; sessions[0]->inflight_q = queues[1];
sessions[0]->incoming_q = queues[2]; sessions[0]->recv_q = queues[3];
struct TRANSIT_QUEUE* transit = (struct TRANSIT_QUEUE*)queue_entry_new(sizeof(*transit) - sizeof(struct ll_entry)); assert(transit);
transit->q = queues[4]; transit->conn = &connection; transit->src_node_id = 11; transit->dst_node_id = 12;
connection.transit_queues = metrics_queue(inst, 0, 0); assert(!queue_data_put(connection.transit_queues, &transit->ll));
struct PKTNORM normalizer = {.input = queues[5]}; connection.normalizer = &normalizer;
expect_router_metrics = 1;
chat_core_collect_status_trampoline(inst);
struct chat_node_arg* req = u_malloc(sizeof(*req)); assert(req); *req = (struct chat_node_arg){inst, 9}; chat_core_collect_conn_metrics_trampoline(req);
assert(text_events == before + 2);
target = "strbuf.c"; fault = F_REALLOCATE; chat_core_collect_status_trampoline(inst); assert(fault == F_NONE && text_events == before + 2);
chat_core_collect_status_trampoline(inst); assert(text_events == before + 3); expect_large_text = 0;
req = u_malloc(sizeof(*req)); assert(req); *req = (struct chat_node_arg){inst, 9};
fault = F_REALLOCATE; chat_core_collect_conn_metrics_trampoline(req); assert(fault == F_NONE && text_events == before + 3);
req = u_malloc(sizeof(*req)); assert(req); *req = (struct chat_node_arg){inst, 9}; chat_core_collect_conn_metrics_trampoline(req);
assert(text_events == before + 4); expect_router_metrics = 0;
const unsigned counts[] = {2, 1, 1, 1, 2, 1};
const size_t bytes[] = {202, 303, 44, 55, 666, 777};
for (unsigned i = 0; i < sizeof(queues) / sizeof(queues[0]); i++) {
assert(queue_entry_count(queues[i]) == (int)counts[i] && queue_total_bytes(queues[i]) == bytes[i]);
assert(!queue_check_consistency(queues[i])); queue_clear(queues[i]); queue_free(queues[i]);
}
queue_clear(connection.transit_queues); queue_free(connection.transit_queues); connection.transit_queues = NULL;
queue_clear(inst->router_conns); queue_free(inst->router_conns); inst->router_conns = saved_router;
connection.normalizer = NULL;
queue_remove_data(inst->connections, entry); queue_entry_free(entry); u_free(links); queue_free(acks);
if (SIZE_MAX > UINT32_MAX) {
size_t baseline = u_get_allocated_count(); struct strbuf text; strbuf_init(&text, (size_t)UINT32_MAX + 1);
assert(!text.buf && !text.cap && !text.len && u_get_allocated_count() == baseline);
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS diagnostics:300 links,full status/metrics beyond8KiB,OOM suppresses partial event,retry,allocator capacity boundary");
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS router metrics:peer/RX scope,unrelated peer excluded,queue bytes/packets,transit,normalizer,null queues,read-only");
}
static void member_limit(struct UTUN_INSTANCE* inst) {
+6
View File
@@ -543,6 +543,12 @@ static void monitor(void* arg) {
uint32_t cycle_target = g_cycle_start_seq + g_cycle_sent;
struct ETCP_ROUTER_CONN* rc = etcp_router_conn_get(cli, TOPO_GROUP_UTUN, server_node_id, TEST_SVC_ID);
if (g_total_rcvd == cycle_target && rc->tx_acked == rc->tx_seq && !rc->send_q->count && !rc->inflight_q->count) {
struct ETCP_ROUTER_TX_STATS stats;
if (etcp_router_tx_stats(cli, TOPO_GROUP_UTUN, server_node_id, TEST_SVC_ID, get_time_us(), &stats)
|| !stats.delivery_rtt_us || !stats.last_ack_us || stats.queued_age_us || stats.inflight_age_us
|| stats.acked_packets != cycle_target) {
printf("[FAIL] Router delivery timing/counters after drain\n"); g_done = -1; return;
}
t_cycle_drain_ms[g_cycle] = toc_ms(&t_phase_start);
printf(" cycle %d drain: %7.1f ms rcvd=%u/%u\n",
g_cycle + 1, t_cycle_drain_ms[g_cycle], g_total_rcvd, cycle_target);
+16 -4
View File
@@ -252,6 +252,19 @@ static void invitations(void) {
assert(!a->send(a->ctx, endpoints[1].inst.node_id, 2, link, strlen(link)));
assert(!b->send(b->ctx, endpoints[0].inst.node_id, 3, link, strlen(link)));
exchange(4); drain();
/* 1000 poll(0) не ждут 15ms BLE ACK: ждём конкретную доставку, а не скорость CPU. */
uint64_t started = get_time_us(), deadline = started + 5000000;
for (int i = 0; i < 2; i++) {
struct ETCP_CONN* c = endpoints[i].conn;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "BLE ETCP invitation overlap: side=%d received=%u target=5 input=%d send=%d ready=%d token=%d",
i, endpoints[i].received, queue_entry_count(c->input_queue), queue_entry_count(c->input_send_q),
etcp_link_io_ready(c->links), c->tx_token != NULL);
}
while ((endpoints[0].received < 5 || endpoints[1].received < 5 || invites_delivered[0] < 2 || invites_delivered[1] < 1) &&
get_time_us() < deadline) uasync_poll(ua, 10);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "BLE ETCP invitation overlap completed: RX=%u/%u invitations=%u/%u elapsed_us=%llu",
endpoints[0].received, endpoints[1].received, invites_delivered[0], invites_delivered[1],
(unsigned long long)(get_time_us() - started));
assert(invites_received[0] == 1 && invites_received[1] == 1);
assert(invites_delivered[0] == 2 && invites_delivered[1] == 1);
assert(delivery_status[0] == LOCAL_DELIVERY_RECEIVED && delivery_status[1] == LOCAL_DELIVERY_RECEIVED);
@@ -266,7 +279,7 @@ static void invitations(void) {
drop_invite_ack = 1;
assert(!a->send(a->ctx, endpoints[1].inst.node_id, 5, link, strlen(link))); drain();
assert(invites_received[1] == 2 && invites_delivered[0] == 4);
uint64_t deadline = get_time_us() + 11000000ULL;
deadline = get_time_us() + 11000000ULL;
while (invites_delivered[0] == 4 && get_time_us() < deadline) uasync_poll(ua, 100);
assert(invites_delivered[0] == 5 && delivery_status[0] == LOCAL_DELIVERY_RECEIVED && invites_received[1] == 2);
hold = 1;
@@ -307,6 +320,7 @@ static void invitation_without_etcp(void) {
}
int main(void) {
debug_config_init(); debug_set_level(getenv("UTUN_TEST_DEBUG") ? DEBUG_LEVEL_DEBUG : DEBUG_LEVEL_WARN);
debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO);
size_t baseline = u_get_allocated_count(); setup();
for (int i = 0; i < 2; i++) {
struct ble_config config = {.node_id = endpoints[i].inst.node_id, .company_id = 0xffff};
@@ -357,9 +371,7 @@ int main(void) {
for (int i = 0; i < 2; i++) {
ble_link_id id = backends[i]->links[0].id;
assert(!ble_tx_ready(backends[i]->owner, id));
struct ll_entry* refused = ll_alloc_lldgram(1); assert(refused); refused->len = 1;
assert(ble_tx_put(backends[i]->owner, id, refused) == BLE_WOULD_BLOCK && !refused->owner);
queue_dgram_free(refused); queue_entry_free(refused);
assert(!etcp_link_io_ready(endpoints[i].conn->links)); /* Без отказа put, который мог бы замаскировать потерю wake. */
}
int before = fragments; drain(); assert(before == fragments); /* Нет busy loop, когда GATT занят. */
if (n == 1) drop = 2; /* Локальный send успешен, но DATA-фрагмент теряется: восстанавливает ETCP. */
@@ -315,16 +315,14 @@ object BleRadio {
BluetoothDevice.PHY_LE_CODED -> "CODED"; else -> "unknown($phy)" }
log(if (status == 0) "INFO" else "WARN", "$role PHY address=$address tx=${name(txPhy)} rx=${name(rxPhy)} status=$status")
}
// 2M — запрос предпочтения. S2/S8 относятся только к CODED PHY, здесь они не применяются.
// 1M: на проверенной Android/Linux паре 2M замедляет очередь контроллера. Результат проверяется callbacks.
private fun preferFastPhy(device: BluetoothDevice, gatt: BluetoothGatt?) {
try {
if (adapter?.isLe2MPhySupported == true) {
if (gatt != null) gatt.setPreferredPhy(BluetoothDevice.PHY_LE_2M_MASK,
BluetoothDevice.PHY_LE_2M_MASK, BluetoothDevice.PHY_OPTION_NO_PREFERRED)
else checkNotNull(server).setPreferredPhy(device, BluetoothDevice.PHY_LE_2M_MASK,
BluetoothDevice.PHY_LE_2M_MASK, BluetoothDevice.PHY_OPTION_NO_PREFERRED)
log("INFO", "PHY 2M requested address=${device.address} outgoing=${gatt != null}")
} else log("INFO", "Local controller has no PHY 2M; retaining negotiated PHY address=${device.address}")
if (gatt != null) gatt.setPreferredPhy(BluetoothDevice.PHY_LE_1M_MASK,
BluetoothDevice.PHY_LE_1M_MASK, BluetoothDevice.PHY_OPTION_NO_PREFERRED)
else checkNotNull(server).setPreferredPhy(device, BluetoothDevice.PHY_LE_1M_MASK,
BluetoothDevice.PHY_LE_1M_MASK, BluetoothDevice.PHY_OPTION_NO_PREFERRED)
log("INFO", "PHY 1M requested address=${device.address} outgoing=${gatt != null}")
if (gatt != null) gatt.readPhy() else checkNotNull(server).readPhy(device)
} catch (e: RuntimeException) { log("WARN", "PHY tuning unavailable address=${device.address}: ${e.message}") }
}
+2 -27
View File
@@ -5,7 +5,6 @@
#include "bluetoothsettingspage.h"
#include "databasesettingspage.h"
#include "statuspage.h"
#include "nodespage.h"
#include "soundsettingspage.h"
#include "audiodevicesettingspage.h"
#include "storagesettingspage.h"
@@ -13,7 +12,6 @@
#include "avatars/avatarmanager.h"
#include "avatars/avatarcrop.h"
#include "ptt_hotkey_manager.h"
#include "connmonitorwindow.h"
#include "../transport/gui_bridge.h"
#include "../db/db_manager.h"
#include "../transport/utun_node.h"
@@ -69,10 +67,8 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Profile"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Storage"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Network"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Connections"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Database"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Status"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Nodes"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Sound"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Audio"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Radio"));
@@ -203,22 +199,6 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod
m_networkPage = new NetworkSettingsPage(m_pages);
m_pages->addWidget(m_networkPage);
// Connections page — button to open standalone window
{
auto* connPage = new QWidget(m_pages);
auto* connLayout = new QVBoxLayout(connPage);
connLayout->setAlignment(Qt::AlignCenter);
auto* hintLabel = new QLabel(QCoreApplication::translate("ChatGui", "Live ETCP connection monitor\nwith per-link BBR statistics"), connPage);
hintLabel->setAlignment(Qt::AlignCenter);
hintLabel->setStyleSheet("font-size: 12px; color: palette(text); padding: 20px;");
connLayout->addWidget(hintLabel);
auto* openBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Open Connection Monitor"), connPage);
openBtn->setStyleSheet("QPushButton { padding: 10px 20px; font-size: 13px; }");
connect(openBtn, &QPushButton::clicked, this, [] { ConnMonitorWindow::showMonitor(); });
connLayout->addWidget(openBtn);
m_pages->addWidget(connPage);
}
// Database page
m_databasePage = new DatabaseSettingsPage(db, m_pages);
m_pages->addWidget(m_databasePage);
@@ -227,10 +207,6 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod
m_statusPage = new StatusPage(node, m_pages);
m_pages->addWidget(m_statusPage);
// Nodes page
m_nodesPage = new NodesPage(db, m_pages);
m_pages->addWidget(m_nodesPage);
m_soundPage = new SoundSettingsPage(db, m_pages);
m_pages->addWidget(m_soundPage);
@@ -340,9 +316,8 @@ void SettingsDialog::onCategoryChanged(int row) {
m_pages->setCurrentIndex(row);
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Settings: selected page=%d; draft retained", row);
// При переключении обновляем только диагностические страницы.
if (row == 4) m_databasePage->refreshStatus();
if (row == 5) m_statusPage->refreshStatus();
if (row == 6) m_nodesPage->refreshNodes();
if (m_pages->currentWidget() == m_databasePage) m_databasePage->refreshStatus();
if (m_pages->currentWidget() == m_statusPage) m_statusPage->refreshStatus();
}
void SettingsDialog::onSave() {
+1 -3
View File
@@ -18,7 +18,6 @@ class BluetoothSettingsPage;
class NetworkSettingsPage;
class DatabaseSettingsPage;
class StatusPage;
class NodesPage;
class SoundSettingsPage;
class AudioDeviceSettingsPage;
class StorageSettingsPage;
@@ -36,7 +35,7 @@ public:
AudioRecorder* recorder, QWidget* parent = nullptr);
private slots:
void onCategoryChanged(int row); // Выбрать страницу; DB/status/nodes обновляются, редактируемый черновик сохраняется.
void onCategoryChanged(int row); // Выбрать страницу; DB/status обновляются, редактируемый черновик сохраняется.
/* Пройти запись/применение страниц и вызвать accept при завершении UI-шагов.
* Команды владеют снимками значений; accept не подтверждает commit SQLite/применение ядром. Общей транзакции нет. */
void onSave();
@@ -62,7 +61,6 @@ private:
NetworkSettingsPage* m_networkPage; // Черновик listen-сокетов и LAN-флагов.
DatabaseSettingsPage* m_databasePage; // Сведения/обслуживание SQLite, обновление при выборе категории.
StatusPage* m_statusPage; // Диагностика ядра/времени, обновление при выборе категории.
NodesPage* m_nodesPage; // Снимок известных узлов БД, обновление при выборе категории.
SoundSettingsPage* m_soundPage; // Черновик звуков событий.
AudioDeviceSettingsPage* m_audioDevicePage; // Выбор устройств/аудиопараметров и тесты микрофона/динамика.
StorageSettingsPage* m_storagePage; // Черновик автозагрузки и лимитов media-хранилища.
@@ -112,7 +112,7 @@ int main(int argc, char** argv) {
auto incoming = [&](uint32_t timestamp) {
for (auto& sample : remote) { rng = rng * 1664525u + 1013904223u; sample = int(rng >> 18) - 8192; }
uint8_t opus[256]; int len = opus_codec_encode(remote_encoder, remote.data(), Samples, opus, sizeof(opus)); CHECK(len > 0);
call_audio_on_media(&inst, 42, seq++, timestamp, opus, len, nullptr);
call_audio_on_media(&inst, 42, seq++, timestamp, opus, len, nullptr, 1);
};
incoming(0); incoming(0); // Настоящий стартовый запас FIFO, приватное состояние не подменяется.
for (unsigned frame = 0; frame < Total; ++frame) {
@@ -64,10 +64,10 @@ int main(int argc, char** argv) {
int16_t pcm[960] = {0}; uint8_t packet[256];
int length = opus_codec_encode(encoder, pcm, 960, packet, sizeof(packet)); CHECK_AUDIO(length > 0);
struct UTUN_INSTANCE foreign = {0};
call_audio_on_media(&foreign, 43, 0, 0, packet, length, NULL);
call_audio_on_media(&foreign, 43, 0, 0, packet, length, NULL, 1);
struct call_jitter_diagnostics diagnostic = {0};
CHECK_AUDIO(!call_audio_get_diagnostics(43, &diagnostic) && diagnostic.encoded_ms == 0);
for (unsigned i = 0; i < 3; ++i) provider->on_media(inst, 43, i, i * 20, packet, length, NULL);
for (unsigned i = 0; i < 3; ++i) provider->on_media(inst, 43, i, i * 20, packet, length, NULL, 1);
CHECK_AUDIO(!call_audio_get_diagnostics(43, &diagnostic) && diagnostic.encoded_ms == 60);
opus_codec_encoder_destroy(encoder);
call_audio_stop(); call_audio_destroy(inst);
+43 -2
View File
@@ -3,6 +3,8 @@
extern "C" {
#include "utun_instance.h"
#include "call/call_audio.h"
#include "chat/chat_setting.h"
#include "opus_codec.h"
#include "debug_config.h"
#include "u_async.h"
#include "mem.h"
@@ -24,15 +26,19 @@ static std::array<uint8_t, CALL_AUDIO_FRAME_SAMPLES> mask{};
static std::atomic<uint64_t> test_time{100000};
static unsigned sent_new, sent_old, extra_new, expected_extra, extra_first = 8;
static std::vector<std::string> messages;
static std::vector<uint8_t> last_packet;
extern "C" uint64_t __wrap_get_time_tb() { return test_time.load(); }
extern "C" int __wrap_chat_setting_get_int(UTUN_INSTANCE*, const char* key, int fallback) {
return !strcmp(key, "call_compressor_enabled") || !strcmp(key, "aec_enabled") ? 0 : fallback;
extern "C" int __real_chat_setting_get_int(UTUN_INSTANCE*, const char*, int);
extern "C" int __wrap_chat_setting_get_int(UTUN_INSTANCE* inst, const char* key, int fallback) {
return !strcmp(key, "call_compressor_enabled") || !strcmp(key, "aec_enabled") ? 0
: __real_chat_setting_get_int(inst, key, fallback);
}
extern "C" int __wrap_call_send_media(UTUN_INSTANCE*, uint64_t id, const uint8_t* data, int len,
const call_capture_time* timing) {
REQUIRE(data && len > 0 && timing);
REQUIRE(id == old_id || id == new_id);
last_packet.assign(data, data + len);
if (id == new_id) ++sent_new; else ++sent_old;
return 0;
}
@@ -69,6 +75,40 @@ static void drain(UASYNC* ua) {
uasync_poll(ua, 0); /* Tasks, опубликованные из extra, исполняются следующим проходом. */
}
/* Сравнение настоящих Opus-пакетов с независимым encoder: сохранённая настройка,
* изменение во время звонка и новый звонок без повторного init frontend. */
static void check_codec_setting(UTUN_INSTANCE& inst) {
REQUIRE(chat_setting_set(&inst, "opus_codec_preset", "0") == 0);
begin(inst, new_id);
auto* reference = opus_codec_encoder_create(48000, 1); REQUIRE(reference);
unsigned frame = 0; uint32_t noise = 1;
for (int preset : {0, 2, 1, 0}) {
if (frame) REQUIRE(chat_setting_set(&inst, "opus_codec_preset", std::to_string(preset).c_str()) == 0);
REQUIRE(opus_codec_encoder_bitrate_set(reference, 16000 << preset) == 0);
REQUIRE(chat_setting_set(&inst, "opus_codec_preset", "3") == -2);
for (unsigned i = 0; i < 12; ++i, ++frame) {
for (auto& sample : pcm) { noise = noise * 1664525u + 1013904223u; sample = int16_t(noise >> 16) / 4; }
uint8_t expected[256];
int size = opus_codec_encode(reference, pcm.data(), pcm.size(), expected, sizeof(expected)); REQUIRE(size > 0);
REQUIRE(feed(new_id, frame, 1) == 1); drain(inst.ua);
REQUIRE(last_packet.size() == size && !memcmp(last_packet.data(), expected, size));
}
}
opus_codec_encoder_destroy(reference); call_audio_stop();
REQUIRE(chat_setting_set(&inst, "opus_codec_preset", "2") == 0);
REQUIRE(call_audio_start_prepared(new_id) == 0);
reference = opus_codec_encoder_create(48000, 1); REQUIRE(reference);
REQUIRE(opus_codec_encoder_bitrate_set(reference, 64000) == 0);
uint8_t expected[256];
int size = opus_codec_encode(reference, pcm.data(), pcm.size(), expected, sizeof(expected)); REQUIRE(size > 0);
REQUIRE(feed(new_id, 0, 1) == 1); drain(inst.ua);
REQUIRE(last_packet.size() == size && !memcmp(last_packet.data(), expected, size));
opus_codec_encoder_destroy(reference); unbind(inst);
REQUIRE(chat_setting_set(&inst, "opus_codec_preset", "1") == 0);
pcm.fill(0); sent_new = sent_old = 0;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call-audio-tx: saved/live/restarted codec 16/32/64 kbit/s packets match reference PASS");
}
int main(int argc, char** argv) {
debug_config_init();
debug_set_level(DEBUG_LEVEL_ERROR);
@@ -102,6 +142,7 @@ int main(int argc, char** argv) {
UASYNC* ua = uasync_create(); REQUIRE(ua);
inst.ua = other.ua = ua;
if (!strcmp(mode, "fresh")) check_codec_setting(inst);
unsigned initial_new = 0, expected_initial = 8;
if (!strcmp(mode, "fresh") || !strcmp(mode, "idempotent")) {
uasync_post(ua, extra, nullptr);
+40 -16
View File
@@ -28,7 +28,7 @@ static int decode_bad(void* arg, const uint8_t*, int, int16_t*, int) {
return -1;
}
static void push(call_jitter* j, int id, int64_t received_ms = -1) {
call_jitter_push(j, (const uint8_t*)&id, sizeof(id), (uint32_t)(id * 20), received_ms < 0 ? id * 20 : received_ms, nullptr);
call_jitter_push(j, (const uint8_t*)&id, sizeof(id), (uint32_t)(id * 20), received_ms < 0 ? id * 20 : received_ms, nullptr, 1);
}
static void test_network_recovery() {
@@ -52,7 +52,7 @@ static void test_network_recovery() {
}
CHECK(largest_reserve >= 60 && state.reserve_ms == 20);
CHECK(state.delay_mode == 0 && state.phase_samples == 0 && state.tempo_ppm == 1000000);
CHECK(state.underruns > 0 && state.underruns <= 2);
CHECK(state.underruns <= 2);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recovery block=%d max_reserve=%dms final=%dms phase=%u underruns=%u",
block, largest_reserve, state.reserve_ms, state.phase_samples, state.underruns);
call_jitter_destroy(j);
@@ -79,7 +79,7 @@ static void test_audio_clock() {
for (int s = 0; s < 960; s++) voice[s] = (int16_t)(8000 * std::sin((sent * 960ll + s) * 0.09));
int len = opus_codec_encode(encoder, voice, 960, packet, sizeof(packet)); CHECK(len > 0);
call_capture_time timing{1000000 + sent * 20000ll, 9, (uint32_t)delay};
call_audio_on_media(&audio_owner, id, (uint16_t)sent, 0, packet, len, &timing);
call_audio_on_media(&audio_owner, id, (uint16_t)sent, 0, packet, len, &timing, 1);
}
CHECK(call_audio_pull_pcm(id, output, 480) == 480); written += 480;
int64_t now = tick * 10000ll;
@@ -115,7 +115,26 @@ static void test_audio_clock() {
CHECK(call_audio_try_clock(id, 43000000, &clock) == -1 && !clock.valid);
std::printf("audio clock: real Opus, burst/tempo, device progress, local tones and reset checked\n");
}
static void test_packet_arrival_sampling() {
decoder d; auto *j = call_jitter_create(decode, &d, 1000);
for (unsigned packet = 0; packet < 4; packet++) {
for (int frame = 0; frame < 3; frame++) {
int id = packet * 3 + frame;
call_jitter_push(j, (const uint8_t*)&id, sizeof(id), packet * 60,
1000 + packet * 60 + frame, nullptr, frame == 0);
}
}
call_jitter_diagnostics diag{}; call_jitter_get_diagnostics(j, &diag, 0);
CHECK(diag.arrival_spread_ms == 0);
int id = 12;
call_jitter_push(j, (const uint8_t*)&id, sizeof(id), 240, 1400, nullptr, 1);
call_jitter_get_diagnostics(j, &diag, 0);
CHECK(diag.arrival_spread_ms == 160);
call_jitter_destroy(j);
}
int main() {
test_packet_arrival_sampling();
CHECK(call_audio_init(&audio_owner) == 0);
{
call_jitter_window w;
@@ -160,10 +179,15 @@ int main() {
decoder d;
auto* j = call_jitter_create(decode, &d, 1000);
CHECK(j);
push(j, 0); // Сохраняем запас 20мс после первого блока, а не расходуем его при старте.
CHECK(call_jitter_pull(j, pcm, 480) == 0);
push(j, 1);
CHECK(call_jitter_pull(j, pcm, 480) > 0);
for (int i = 0; i < 9; i++) {
push(j, i);
CHECK(call_jitter_pull(j, pcm, 480) == 0);
CHECK(d.seen.empty()); // До200мс не декодируем и не расходуем накопленный звук.
}
call_jitter_get_stats(j, &depth, nullptr, nullptr, &under, nullptr);
CHECK(depth == 180 && under == 0);
push(j, 9);
CHECK(call_jitter_pull(j, pcm, 480) == 480);
call_jitter_diagnostics diagnostic{};
call_jitter_get_diagnostics(j, &diagnostic, 0);
CHECK(diagnostic.resumed == 1 && diagnostic.pcm_samples == 480 && diagnostic.reserve_ms == 20);
@@ -175,13 +199,13 @@ int main() {
call_jitter_get_diagnostics(j, &diagnostic, 0);
CHECK(diagnostic.resumed == 0);
call_jitter_range range;
push(j, 2);
for (int i = 3; i < 503; i++) push(j, i, 10060); // 10с сетевой задержки, затем пачка
push(j, 10);
for (int i = 11; i < 511; i++) push(j, i, 10220); // 10с сетевой задержки, затем пачка
call_jitter_get_stats(j, nullptr, nullptr, nullptr, nullptr, &range);
CHECK(range.reserve_ms == 1000);
int next = 503, highest = 100;
int next = 511, highest = 100;
for (int tick = 0; tick < 6000; tick++) {
if (tick % 2 == 0) push(j, next++, 10060 + tick * 10); // непрерывный live
if (tick % 2 == 0) push(j, next++, 10220 + tick * 10); // непрерывный live
call_jitter_pull(j, pcm, 480);
call_jitter_get_stats(j, &depth, &tempo, &dropped, &under, nullptr);
if (tempo > highest) highest = tempo;
@@ -298,10 +322,10 @@ int main() {
for (auto value : pcm) CHECK(value == 0);
}
// Первый пакет ещё не заполнил запас PCM: продолжается тихое ожидание.
call_audio_on_media(&audio_owner, id, 0, 0, encoded, len, nullptr);
call_audio_on_media(&audio_owner, id, 0, 0, encoded, len, nullptr, 1);
CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
for (auto value : pcm) CHECK(value == 0);
for (int i = 1; i < 6; i++) call_audio_on_media(&audio_owner, id, i, 0, encoded, len, nullptr);
for (int i = 1; i < 10; i++) call_audio_on_media(&audio_owner, id, i, 0, encoded, len, nullptr, 1);
CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
bool audible = false;
for (auto value : pcm) if (value) audible = true;
@@ -314,14 +338,14 @@ int main() {
}
CHECK(audible);
// Малого запаса недостаточно для восстановления: сигнал продолжает звучать.
call_audio_on_media(&audio_owner, id, 6, 0, encoded, len, nullptr);
call_audio_on_media(&audio_owner, id, 10, 0, encoded, len, nullptr, 1);
audible = false;
for (int tick = 0; tick < 100; tick++) {
CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
for (auto value : pcm) if (std::abs((int)value) > 1000) audible = true;
}
CHECK(audible);
for (int i = 7; i < 30; i++) call_audio_on_media(&audio_owner, id, i, 0, encoded, len, nullptr);
for (int i = 11; i < 34; i++) call_audio_on_media(&audio_owner, id, i, 0, encoded, len, nullptr, 1);
CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
call_audio_begin_end(id); // явное завершение прекращает догон и включает финальный тон
CHECK(call_audio_pull_pcm(id, pcm, 480) > 0);
@@ -337,7 +361,7 @@ int main() {
uint8_t encoded[512];
int len = opus_codec_encode(encoder, silence, 960, encoded, sizeof(encoded));
CHECK(len > 0);
for (int i = 0; i < 6; i++) call_audio_on_media(&audio_owner, 124, i, 0, encoded, len, nullptr);
for (int i = 0; i < 10; i++) call_audio_on_media(&audio_owner, 124, i, 0, encoded, len, nullptr, 1);
std::vector<int16_t> output(777);
under = 0;
for (int tick = 0; tick < 30 && !under; tick++) {