Files
utun2/doc/tasks.md
T

599 lines
62 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Задачи по проекту
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.
## Текущая задача
[ ] **ChatGUI Windows: проверка линковки ZXing без -Os** — 05.10.2026:
после удаления старых DLL и чистой сборки линкер не находит move-конструктор
`std::string` в `libZXing.a(Barcode.cpp.obj)`. ZXing задаёт этому файлу -Os;
такая ошибка описана в https://github.com/msys2/MINGW-packages/issues/31015.
Для MinGW GCC отключён пофайловый -Os; используются флаги выбранного типа сборки.
Проверена генерация команд: при MinGW+GNU Barcode.cpp получает -O3 без -Os,
Linux сохраняет прежний -Os. Компиляция Barcode.cpp на Linux прошла.
Требуется Windows-проверка обычным `tools/chatgui/build.bat`.
[ ] **ChatGUI Windows: подтверждение обновления runtime компилятора** — 05.10.2026:
`cc-probe.log`: cc1 запускается из корня, но в `tools/chatgui/build` завершается
с `0xC0000139`; локальные DLL GCC/math/runtime отличаются от UCRT64.
Перед обеими точками сборки добавлено обновление существующих копий этих DLL;
раскладка FFmpeg использует для них UCRT64. Проверены обновление, повторный вызов,
сохранение кодеков и ошибки. Требуется Windows-прогон `check_cc.bat`: cc1 после
обновления, повтор команды Ninja и сборка Opus должны завершиться с кодом 0.
[ ] **ChatGUI Windows: обновление остальных DLL приложения после обновления MSYS2** — 05.10.2026:
в `cc-probe.log` также отличаются Qt и другие DLL. `tools/chatgui/build.sh`
пропускает раскладку при наличии основного комплекта, без сравнения версий;
рекурсивная раскладка копирует только отсутствующие зависимости.
Runtime GCC обновляется отдельно; для остальных DLL нужно продумать обновление
с сохранением приоритета собственной сборки FFmpeg и проверить запуск приложения.
[+] **Открытие HTTP/HTTPS в сообщениях Android и Qt** — 04.10.2026:
общая обработка ссылок по диапазонам текста; веб-адреса открываются системным
обработчиком, `utun://` сохраняет JOIN и короткое отображение PM-приглашений.
Учтены Unicode, IPv6, скобки, пунктуация, параметры, fragment и emoji в адресах.
Qt использует одинаковый layout для отрисовки, размеров, нажатия и выделения.
Ошибки открытия переведены и логируются. Общие 15 случаев прошли в Android и Qt;
проверки нажатия/drag/copy и приглашений прошли. Чистые сборки Qt и APK — PASS.
`check.sh`: 124 passed, 0 failed, 1 skipped; proxy/burst/load — PASS.
APK установлен на Q8, проверено системное открытие HTTP и HTTPS через ACTION_VIEW,
приложение запущено. Логи: `/tmp/message-links-check.log`,
`/tmp/message-links-qt-final-tests.log`, `/tmp/message-links-android-tests.log`.
Контракт: `doc/message_links.md`.
[+] **Заголовок PM-приглашения и короткое отображение ссылки** — 04.10.2026:
`ct=invite` содержит зашифрованные название группы и полную ссылку; GUI получает
название через `tags.group_name` и добавляет заголовок из перевода интерфейса.
Android и Qt отображают ссылку одной строкой с многоточием. Нажатие и копирование
используют полный адрес: долгое нажатие в Android, выделение строки и Ctrl+C в Qt.
Срок действия ключа — 10 минут. Проверена доставка названия с Unicode, кавычками
и переносом строки, JOIN и история; Qt проверяет разные ширины, нажатие на конец
сокращённой ссылки и копирование. Чистые сборки ядра, Qt и APK прошли.
`check.sh`: 124 passed, 0 failed, 1 skipped; proxy/burst/load — PASS.
Qt `test_invite_links` и `test_emoji_playback` — PASS. APK установлен на Q8,
приложение запущено. Логи: `/tmp/pm-invite-title-check.log`,
`/tmp/pm-invite-title-e2e-final.log`. Контракт: `doc/pm_invites.md`.
[+] **История группы после PM-приглашения** — 04.10.2026: регистрация `db_sync`
создаёт локальное состояние с блокировкой обмена; история запускается после
успешного member_sync через READY-пиров конкретной группы. Подписка на переходы
READY сбрасывает обмен при DOWN/REINIT/LEAVE и возобновляет его при восстановлении.
Обе роли имеют 15-секундный таймаут; ошибки отправки и отказ пира разрешают повтор.
JOIN_INFO_RESP применяет участника через транзакционный member_sync, обновляя
Merkle-индекс вместе с моделью. ASAN также выявил и помог исправить использование
временного content_type после выхода из области видимости при выдаче JSON истории.
Проверены пять сообщений до приглашения, оба порядка ID, потеря первого обмена,
восстановление READY на общем транспорте и последующая доставка новых сообщений.
Эти сценарии, ownership и services прошли под ASAN. Финальный check.sh:
124 passed, 0 failed, 1 skipped; интеграционные proxy/burst/load — PASS.
Чистые сборки ядра, Qt и APK прошли. На Q8 после обновления
существующий `g2` получил все 5 старых сообщений без повторного JOIN;
SYNC_DONE подтвердил совпадение хеша, SQLite quick_check — ok.
Диагностика: `/tmp/pm-history-q8-final.log`, `/tmp/pm-history-asan-invite.log`,
`/tmp/pm-history-asan-timeout.log`, `/tmp/pm-history-final-check.log`.
Контракт: `src/chat/db_sync_arch.md`.
[+] **Android: диагностика несрабатывания VAD** — 03.10.2026: в `debug` добавлены
решения по каждому изменению состояния и сводки PCM/AGC/модели раз в секунду;
`debug=trace` показывает каждое окно 32мс. Логи различают сброс подтверждения,
чужой RX, закрытое окно Android, задержки inference и отложенный BEGIN.
Регрессии radio_vad, Silero, radio_audio и voice_tx прошли. На Samsung в 19:36:23
подтверждён отказ после истечения 15-минутного окна: `reason=window_expired`,
`requested=true screen=true capture=false nativeVad=false`; ручной PTT работает.
Кнопка на панели показывает сохранённое включение, а не фактическую активность VAD.
Лог: `/tmp/vad-sm-current.log`. Исправление таймера и индикации — следующая запись.
[+] **Android: индикация и фоновый таймер VAD** — 03.10.2026: кнопка показывает
фактическую активность захвата, гаснет при его остановке; повторное включение
открывает новое окно. 15-минутный таймер расходуется только в фоне, при видимом
приложении сбрасывается: после каждого сворачивания доступны полные 15 минут.
После истечения возврат в приложение возобновляет VAD; погасший экран по-прежнему
останавливает микрофон, включение экрана открывает новое окно.
`tests/test_radio_vad_window.kt` проверяет 2 часа в foreground, сброс таймера при
возврате, полное фоновое окно, границу истечения, экран и сохранение окна при перенастройке.
На Samsung проверены фактические lifecycle-переходы и выключенная кнопка после
остановки захвата: `/tmp/vad-background-sm-transitions.log`. Чистая сборка APK прошла,
Samsung SM-A525F и Q8 обновлены; на Samsung восстановлены прежний канал и активный VAD.
Уточнение: при возвращении в приложение таймер теперь полностью сбрасывается,
соответствующая регрессия прошла. Новая чистая сборка — `/tmp/vad-reset-android-build.log`,
установлена на оба телефона. Повторная проверка lifecycle на Samsung после этого
обновления не выполнена: телефон заблокирован; правило сброса проверено тестом с искусственным временем.
[ ] **Android: время жизни config provider** — 03.10.2026: `utun_bridge_init()`
передаёт адрес локального `utun_config_provider_t` в `utun_config_set_provider()`,
который хранит указатель. После возврата из init он недействителен; перед
использованием `cfg_get_*` обеспечить постоянное хранение provider.
[+] **DM: надёжная доставка сообщений, первый этап pm.txt** — 01.10.2026:
атомарные история/seq/outbox, подписанные точные квитанции после commit,
live BGP-маршрут и durable очередь на суперузле с проверкой подписанной роли.
Повторы, out-of-order, rollback БД, неверные ACK и поздний PUT проверены;
40 offline-сообщений доставлены после restart источника, первый offline DM —
после restart суперузла при выключенном источнике. Копии удалены по квитанции.
Подготовка ciphertext медиа использует ключ target и независимые nonce порций;
проверены границы 32 КиБ, пустые файлы, неверные ключи/UUID/hash и повреждения.
Clean-сборка ядра/Qt, test_dm, test_dm_e2e и полный check прошли:
113 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root); proxy/burst/load PASS.
Текущие гарантии и оставшаяся интеграция: `doc/dm_arch.md`.
[+] **DM: сетевая доставка и pending-медиа по pm.txt** — 01.10.2026:
отдельные owners dm_media/dm_mailbox_media и файловый transfer с CM handle,
подписанные метаданные и независимая квитанция файла после durable приёма.
Прямая доставка или S→M custody, restart S/M при выключенном A, доставка B,
повтор удаления до DELETE_ACK; поздние STORE/STORED не возрождают копии.
TTL M по умолчанию 7 дней, резервирование квоты, отмена transfer и stop workers.
Копирование, шифрование, хеширование, проверка и durable файловые операции —
в media_async; отправка по 8 КиБ через call_soon. Тест 8 МиБ/0,5 с сохраняет
heartbeat uasync (максимальный интервал 10,3 мс в проверенном прогоне).
Qt/Android: оригинальные файловые вложения, сохранение и статусы доставки;
PM-снимки из ядра асинхронные. Чистые сборки Qt и Android APK прошли.
test_dm_media/test_dm_worker, rollback/quota/cancel и полный check:
115 PASS, 0 FAIL, 1 SKIP; proxy/burst/load PASS. GUI на устройствах не проверялся.
Протокол, владение и проверки: `doc/dm_arch.md`.
[+] **DM: голосовые/видео и общий GUI вложений** — 01.10.2026:
тип FILE/VOICE/VIDEO и метаданные зашифрованы ключом target; в transport остаётся
подписанный descriptor, без новых ключей и канального протокола в DM.
Общие attachment/voice_file/attachment_send: Opus, waveform, копирование и видео
готовятся в workers; completion через reserved post вместо polling. PCM передаётся
без копии; адресат закреплён до записи/выбора. Отмена освобождает собственные файлы.
Android: ConversationComposer/ConversationMessages для каналов и PM, CameraX MP4,
IO для URI/метаданных; Qt: общий InputBar/delegate, FFmpeg, фоновые превью/Opus.
Подготовка адресована беседе и UUID задачи; статусы доставки показаны для всех типов.
test_attachment/test_attachment_send проверяют кодек, формат, владение и отмену;
test_dm_media — voice напрямую и encrypted video metadata после offline restart.
Чистые сборки ядра/Qt/APK, 117 PASS / 0 FAIL / 1 SKIP в check.sh; proxy/burst/load PASS.
Qt CTest: 7 PASS, включая повреждения Opus и отмену decoder. APK не устанавливался,
интерактивная проверка устройств не выполнялась. Подробности: `doc/dm_arch.md`.
[ ] **uasync: ложные ERROR об утечке перед cleanup** — 01.10.2026:
test_dm_e2e и существующий test_conn_mgr проходят, но uasync_destroy()
пишет Timer leak до освобождения отменённых heap/immediate таймеров.
После cleanup счётчики сходятся, финальная проверка проходит. Перенести
предварительные счётчики в DEBUG; ERROR оставить для реального расхождения
после cleanup, отдельно показывать активные и отменённые таймеры.
[+] **Статус подключения звонка в Qt и Android** — фактический путь router:
прямое / reverse / indirect / нет подключения. Учитывается BGP fallback;
события только при изменении, общий таймер 1с, сброс при завершении.
Проверены смена путей настоящего звонка, медиа, ASAN, Qt, Android APK и полный check.
[+] **Единицы RTT в окне звонка** — `call_post_stats()` переводит
`ETCP_CONN.rtt_last` из 0.1мс в миллисекунды до отправки CALL_STATS.
Исправление общее для Qt и Android. DEBUG-категория `debug` раз в секунду
показывает исходный RTT, RTT в мс, транспортного соседа и метрики аудио.
[+] **Восстановление путей conn_mgr на протяжении жизни handle** — начальный
TIMEOUT отделён от срока цикла проб. Поздний обрыв запускает новый цикл;
рабочий резерв и NCD сохраняются, при отсутствии резерва INDIRECT стартует сразу.
Циклы ограничены 15с, паузы 1→2→4→8→15с; новая версия узла или локальные сокеты
обходят паузу. Прямой READY останавливает поиск и получает приоритет.
Исходящие request_id и входящие сроки независимы; устаревшие ответы игнорируются.
UP/DOWN отражают общую доступность, TIMEOUT приходит один раз до первого UP.
Последний close отменяет восстановление, включая закрытие из callback.
Проверены десять сетевых сценариев с реальными данными: поздний обрыв,
повторные циклы, смена адресов/сокетов, потеря маршрута посредника, старый ACK,
начальный TIMEOUT с поздним UP и close. ASAN без ошибок; собраны Qt и Android APK.
Полный check: 113 PASS, 0 FAIL, 1 SKIP; пропущенный auto_socket_dynamic отдельно
прошёл с root в network namespace. Интеграционные proxy, burst и load прошли.
Алгоритм: `src/routing_layer/conn_mgr_doc.md`.
[ ] **Общий лимит check.sh после clean** — 01.10.2026: расчёт по времени предыдущего
прогона дал 205с вместе с полной сборкой и оборвал проверку после успешных тестов
до `test_uasync_socket_race`. Повторный запуск на готовой сборке с большим лимитом
прошёл за 206с, включая proxy/burst/load. Разделить бюджет сборки и запуска тестов
либо учитывать clean при расчёте.
[ ] **Паузы аудио Qt при звонке с SM** — 01.10.2026, звонок
`939a3ea36f3eeac3`, прямой LAN TCP `192.168.40.250:44604 ↔ 192.168.40.247:44173`.
Tcpdump: общие паузы входящего трафика до 645мс; ping до роутера в среднем
100мс, до SM 128мс. ПК на 2.4ГГц, SM на 5ГГц. Одновременно `CallAudio: cb`
падает до 20–40/с вместо 100/с, буфер Qt растёт до 2с, на SM повторяется refill.
В 8с strace аудиопоток продолжает обслуживать PulseAudio без долгих syscall;
причина редких аудиоколлбэков пока не установлена. Добавить измерения интервалов
и длительности callback, кадров capture/playback и сводку CALL_STATS; сравнить
сетевые задержки после подключения ПК к 5ГГц или кабелю.
Повторный звонок `b2a423ed2d36ed83` (01:25:31–01:27:39): DIRECT сохраняется,
RTT 2–3мс; в начале на SM gap tone длится 1.8–2.0с примерно каждые 6с.
На Qt частота callback падает вплоть до 4/с, входящий PCM продолжает приходить:
буфер 65→465→1465→2079мс за 3с, затем растёт до нескольких секунд, dropped=0.
Локализовано прекращение обслуживания duplex-аудио Qt; ожидание backend и
задержка обработки PCM пока не разделены. Часы SM отстают от Qt примерно на 1.24с.
Добавлена диагностика backend/устройств, времени между callback и длительности
capture/playback, фактической частоты callback и PCM кадров за интервал.
Счётчики сбрасываются до старта устройства; нужна повторная запись с новой сборкой.
[+] **Ложный сигнал потери звука при запуске звонка** — 01.10.2026, звонок
`bc3efadcac629947`: Qt запускает аудиоколлбэки в 01:54:56.933, включает gap tone в
01:54:57.415 и выключает в 01:54:58.072 при первом воспроизведении PCM.
На SM между ACCEPT и первым захваченным кадром проходит около 1.15с;
часы SM отстают примерно на 1.23с. Qt стабильно обслуживает 100 callback/с,
RTT около 1мс. Исправлено: до первого PCM начальное ожидание проходит в тишине,
последующий перебой включает тон через 500мс. Стартовый сбой по-прежнему
ограничен существующим watchdog отсутствия медиа. Регрессия проверяет ожидание
1.5с, начальное накопление, настоящий перебой, неполный блок, новый звонок и завершение.
Ускорение запуска Android: router.start занимает около 775мс; система сообщает
длительное удержание AudioManager.updateAudioPortCache. Ещё около 370мс проходит
до запуска capture worker. Гудки теперь останавливаются до startAudio;
router использует снимок устройств с обновлением по событиям и явной переоценке,
capture запускается до подготовки/старта playback. Ошибка playback останавливает capture.
Добавлены длительности отдельных стадий, первого capture и PCM. Ядро, Qt и APK
собраны после clean; три аудиотеста прошли. Выигрыш на SM требует замера в новом звонке.
В звонке Qt `180f046ef22defc2` первый PCM пришёл через 1245мс без стартового gap tone.
Полный check вне песочницы: 113 PASS, 0 FAIL, 1 SKIP; auto_socket_dynamic отдельно
прошёл с root в private netns. Интеграционные proxy, burst и load прошли.
[ ] **Оставшаяся задержка запуска аудио SM** — после обновления 01.10.2026,
звонок `2b9ad4e21a54d8ba`: стартового gap tone нет на обеих сторонах; Qt
стабильно работает около 100 callback/с, max_idle=15.3мс, буфер 55мс, RTT=2мс.
На SM первый capture появляется через 1140мс от startAudio (1164мс от ACCEPT).
router.start=781мс, получение снимка устройств=0мс, AudioRecord create=24мс,
startRecording=7мс; от начала startRecording до первого кадра=328мс.
Capture worker стартует раньше playback, но общий выигрыш пока не виден.
AudioTrack create=21мс, play=327мс; система сообщает AudioPortCache contention=497мс
и восстановление AudioTrack при setPreferredDevice. Разделить замеры регистрации
callback, BT-профиля и применения маршрута; отдельно проверить первый AudioRecord.read.
На SM входящий буфер за время подготовки вырос до 1149мс и к концу звонка снизился
до 570мс; ускорение старта playback также сократит начальное накопление.
Реализован независимый запуск AudioTrack и AudioRecord в play/capture потоках;
частичный старт отменяется до освобождения устройств/native. AEC измеряется после готовности обоих.
Добавлены callbackMs/btProfileMs/applyMs/refreshMs, длительность первого read и время первой записи playback.
Резерв теперь зависит от изменения задержки доставки по timestamps CALL_MEDIA, отдельно от глубины очереди;
стартовое накопление и догон не повышают запас. Сохранность FIFO и uint32 wrap проверяются регрессиями.
Сборки ядра, Qt и Android после clean успешны; три аудиотеста прошли. Полный check:
113 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root); proxy/burst/load прошли.
Регрессия refill учитывает остаток SoundTouch: два новых кадра могут уже заполнить запас 60мс.
SM и Q8 обновлены 01.10.2026 в 03:03. Выигрыш по времени требует пробного звонка с новой версией.
[ ] **Гонка invite-файла в test_chat_join_e2e** — `wait_file()` проверяет только
существование файла, а `wf()` создаёт его до записи. Читатель иногда получает
пустую ссылку и ошибку префикса `utun://`. Публиковать готовый файл атомарно.
[ ] **Некорректное время в выводе check-proxy** — успешные `basic_1mb` и
`stress_sessions` иногда печатают огромные положительные/отрицательные миллисекунды,
хотя итоговая скорость и длительность передачи выглядят нормально. Проверить расчёт вывода.
[+] **SpeexDSP AEC (эхоподавление) в lib/** — акустическое эхоподавление звонка.
Вендорен SpeexDSP (mdf/fftwrap/kiss_fft, 3-clause BSD) в `lib/speexdsp/`,
обёртка `lib/speex_aec.c/h` (C-API, FLOATING_POINT + USE_KISS_FFT, без внешних
зависимостей). Собственная линия задержки рендера на `delay_frames` кадров +
синхронный `speex_echo_cancellation` (встроенный буфер SpeexDSP фиксирован в
2 кадра и для Android не годится); дрейф потоков — через переполнение/недозаполнение
линии (drop/dup + passthrough). Категория лога `aec`.
Ядро: `call_audio_set_aec_enabled/delay_frames`, AEC в feed_pcm (захват→encode)
и pull_pcm (рендер-референс), lifecycle в start/release. Desktop — настройка GUI
`aec_enabled` (`[chatserver]`, читается в `call_audio_start`). Android — включается
только на спикерфоне (`route==SPEAKER`, live через JNI `callAudioSetAec`),
задержка меряется `AudioTrack/AudioRecord.getTimestamp`; `CallAudioEngine.playLoop`
переведён на absolute-deadline (устранён систематический дрейф). Тест
`test_speex_aec` (подавление эха 25.5 дБ, сохранность речи 0.956, delay-line, NULL-гарды).
Сборки: autotools + chatgui CMake + Android CMake/APK.
[+] **Silero VAD в lib/** — добавлен детектор голосовой активности (Silero VAD v5,
стриминговая ONNX-модель + обёртка `lib/silero_vad.c/h` над ONNX Runtime C API).
API: `silero_vad_create/create_default/destroy/reset/process/process_pcm16`
(окно 512 сэмплов @16 кГц, вероятность речи [0..1], рекуррентное состояние).
Модель вшита в бинарник (`silero_vad_model.inc`, `create_default`), файл не нужен.
Категория лога `vad`. Модуль под `#ifdef HAVE_SILERO_VAD` — без onnxruntime
компилируется в стабы, поэтому GLOB-сборки (chatgui/Android) не ломаются.
Сборка: `--with-silero-vad` (autotools), `-DHAVE_SILERO_VAD` + libonnxruntime
(chatgui CMake, Android NDK). `make check`: 111/0/1.
[+] **Авто-PTT рации по VAD** — сделано. Отдельный режим `radio_vad_enabled` (ручной
PTT не меняется, при зажатой кнопке — override). Сервисный слой `radio_audio.c`:
ресемплер 48k→16k моно (3-tap box + 3:1, стерео микшируется), накопитель окон
512, фильтр-гистерезис + машина состояний (`src/radio/radio_vad.h`, чистый,
юнит-тестируемый): старт при подтверждении 2 окон (~64мс) выше порога
`radio_vad_threshold`, стоп при тишине `radio_vad_hangover_ms` (200мс) или чужом
разговоре (busy = RX-кадр <200мс). Перед BEGIN сохраняется до 600мс непереданного PCM;
prefetch отправляется одной задачей перед live без повторов и задержки на стороне отправителя.
VAD использует отдельный компрессор с собственной историей усиления. Desktop `radio_audio_engine.cpp` и Android
`RadioAudioEngine.kt` фидят PCM постоянно в VAD-режиме; JNI `radioVadMode()`.
Тест `test_radio_vad` (ресемплер + FSM). Android: onnxruntime AAR 1.23.2 в
`jniLibs/` + `cpp/onnxruntime/include` + `-DHAVE_SILERO_VAD` в CMake.
Android: настройки VAD (включение, порог, задержка после тишины); микрофон в PTT
создаётся только для передачи. PTT запускается при касании, после отпускания
передаёт ещё 250мс; повторное касание отменяет завершение (двойной тап без FIN).
VAD работает 15 минут при горящем экране;
SCREEN_OFF освобождает захват, SCREEN_ON автоматически открывает новое окно.
RX остаётся активен; TX освобождается отдельно через `radio_audio_capture_stop()`.
[+] **Отдельный компрессор перед VAD рации** — 03.10.2026:
копия PCM 48кГц/20мс проходит через общий audio_compressor до ресемплера и Silero.
Настройки общие с TX, кроме `radio_vad_compressor_max_gain_db` (15дБ по умолчанию,
1..60дБ); включение также общее. Состояния TX/VAD независимы, prefetch хранит исходный PCM.
Выключенный компрессор не добавляет ожидания блока; разрыв/новый захват сбрасывает AGC и Silero.
Android: отдельный ползунок, DataStore/INI/runtime; диагностика `radio` показывает
параметры, input/output RMS, фактический gain и клиппинг. Диапазоны max_gain согласованы с библиотекой.
test_voice_tx: точный вход Silero, байтовое совпадение Opus/prefetch, mono/stereo,
6/30дБ, переменные PCM-чанки и reset. Настоящий Silero распознаёт речь при амплитуде
0.03 на тихом фоне, останавливается после речи; повторный reset воспроизводит результат.
Чистые сборки ядра/Qt/APK, целевые тесты и полный check.sh прошли:
124 PASS / 0 FAIL / 1 SKIP, proxy/burst/load PASS; Qt CTest 2/2, AGC 479/479.
APK с последующим переносом переключателя VAD установлен на Samsung SM_A525F.
[+] **Включение VAD на панели рации Android** — 03.10.2026:
кнопка VAD расположена слева от правой кнопки плавающего PTT; состояние читается из DataStore Flow.
Действие в ViewModel сохраняет настройку, обновляет ядро, запрашивает разрешения и перенастраивает захват.
Переключатель удалён из настроек; порог речи, задержка после тишины и max gain оставлены.
Чистые сборки ядра/Qt/APK прошли, APK установлен на Samsung; UI настроек проверен через uiautomator.
[+] **Верхняя панель рации и VAD в desktop ChatGUI** — 03.10.2026:
строка под шапкой канала: меню микрофона/выхода, состояние, переключатель VAD слева от PTT.
PTT перенесена с сообщений в панель; подсветка показывает фактический TX, включая автоматический VAD.
Настройки рации: вероятность речи, задержка тишины, включение AGC и независимые max gain TX/VAD.
Общий стек сохраняет prefetch 600мс; настройки применяются в uasync с подтверждением в GUI.
Перезапускается только capture, RX/output сохраняются; удерживаемые кнопка/хоткей восстанавливаются.
INI сохраняется атомарно; ошибка микрофона/детектора видна на панели, ручной PTT доступен без VAD.
Запрос настроек принадлежит reserved uasync task и освобождается также при shutdown до исполнения.
Исправлено сохранение Audio без посещения страницы: устройства и аудиопараметры предварительно загружаются.
Чистые сборки ядра/Qt и Qt CTest 16/16; test_radio_panel проверяет Qt-кнопки и настоящие audio workers
с null backend: auto-TX, 30 кадров prefetch, manual/hotkey, capture recovery, INI restart и отмену запроса.
Полный check.sh: 124 PASS / 0 FAIL / 1 SKIP, proxy/burst/load PASS; настоящий Silero распознаёт тихую речь.
Логи: `/tmp/desktop-vad-qt-verified-tests.log`, `/tmp/desktop-vad-core-check-unrestricted.log`.
[+] **PTT кнопками громкости на панели рации Android** — 03.10.2026:
переключатель слева от VAD, включённое состояние подсвечено; панель и настройки используют общий DataStore Flow.
Значок: динамик по центру, плюс сверху, минус снизу; проверен на панели Samsung после чистых сборок ядра/Qt/APK.
Сохранение обновляет быстрый кэш после записи; ошибки записи логируются без изменения состояния переключателя.
Экранная кнопка, оверлей и кнопки громкости имеют независимые владельцы ручной передачи.
Учёт отдельных volume-клавиш защищает от повторов и посторонних клавиш; потеря фокуса/выключение опции отменяет volume-PTT.
Чистые сборки ядра/Qt/APK прошли, 12 сценариев владельцев PTT проверены отдельным Kotlin-прогоном.
APK установлен на Samsung SM_A525F: обе volume-кнопки запускают запись, отпускание завершает её с хвостом 250мс.
В отдельном канале без других участников проверены переключатель и сохранение выключенного состояния после restart.
Логи: `/tmp/android-volume-ptt-final-device.log`, `/tmp/android-volume-ptt-apk-final-build.log`.
[ ] **Linux ONNX Runtime: повторная регистрация схем при запуске теста** — 03.10.2026:
test_silero_vad при загрузке модели печатает серии `Schema error: ... already registered`.
Инференс, речь, тишина и reset проходят; отдельно проверить состав и загрузку Linux-библиотек ONNX.
Диагностика: `/tmp/vad-compressor-silero-tests.log`; Android-сборка таких сообщений не содержит.
[ ] **test_tcp_proxy_full: некорректное время в диагностике** — 03.10.2026:
полный check.sh проходит, но строки basic/stress/concurrent/half_close выводят
огромные или отрицательные миллисекунды (например, -1611883348002232012ms).
Отдельно проверить формат/аргументы вывода длительности; `/tmp/vad-compressor-check.log`.
[+] **chatgui (Qt) не собирался: C++ void* в utun_node.cpp** — сделано. Причина:
`proxy_protocol.h` (C-заголовок, включается в C++ `utun_node.cpp`) делал
`e->dgram = u_malloc(...)` и `struct proxy_flow* f = arg;` без явных приведений —
в C++ нет неявного `void*`→T*. Добавлены `(uint8_t*)`/`(struct proxy_flow*)`
(валидны и в C). `vibechat` собирается (47МБ, с вшитой моделью + onnxruntime).
[+] **test_auto_socket_dynamic: root-регрессия и нагрузка** — изолированный namespace,
корректные маршруты, двусторонний поток через backpressure, строгие seq/payload,
пределы TX/RX, остановка при обрыве и drain после восстановления, баланс ресурсов.
Исправлены привязка TCP к интерфейсу, удаление TCP моделей/линков/портов из БД,
sender receive window при selective ACK, timestamp overflow и нулевой jitter.
ASan/UBSan/LSan: PASS; дополнительно исправлены невыровненные метки аллокаторов
и PID-порты в test_etcp_connect. [Разбор и логи](etcp_full_suite_findings.md).
[ ] **debug_parse_config: формат и индексы категорий** — parser ожидает `:`, документация
описывает `=`; преобразование битовой маски осталось после перехода enum на индексы.
[Разбор](etcp_full_suite_findings.md).
[+] **Q8/Android не подключается к локальному чатгуи (LAN)** — сделано. Причина: тип сокета
в Android-конфиге был захардкожен `type=public`, из-за чего `sock_match` ставил
`has_priv=0` и не создавал линки к приватным (LAN) адресам пиров (`create_links … → 0 links`).
Исправлено: (1) `ip_is_public()` вынесен в `lib/platform_compat` (единый классификатор
IPv4 с корректным ntohl); (2) в `auto_socket_reconcile_config` (Android-режим) тип сокета
выставляется по адресу (public→PUBLIC, приватный/link-local→NAT), конфиг `type` игнорируется;
(3) `sock_collect_views` больше не смотрит на `type` из конфига: `has_priv` — по классификации
локального адреса, `has_inet=1` (исходящие считаем доступными), `is_eim` — по NAT-детекции
(hairpin). `is_local_subnet` упрощён до `!ip_is_public()`. Тест `test_sock_match` обновлён
(+ `ip_is_public` кейсы). `check.sh`: 86 passed / 1 failed / 1 skipped.
[ ] **DM: прямой p2p чат с любым пользователем группы** — отдельная DM-подсистема.
ТЗ и архитектура: `/doc/dm_arch.md`. Статус: реализовано (dm_core/dm_mailbox/dm_crypto),
тесты `test_dm` (крипто) и `test_dm_e2e` (интеграция). Остались задачи ниже.
[ ] **Звонок (P2P аудио)** — голос 1:1, из CHAT-группы или p2p-чата (DM). Отдельный
каталог `src/call/`. Сигналинг + conn_mgr + аудиопоток (Opus) с backpressure
(локальный буфер отправки при переполнении роутера) + адаптивный джиттер-буфер
(SoundTouch, ускорение до 1.6x) + тоны (глитч/завершение) + watchdog 20с.
Android: переключение источника (трубка/громкая/гарнитура).
Архитектура: `/doc/chat_call_arch.md`.
[+] **Сигналинг + релей медиа** — сделано. `call_proto.h` (svc `ETCP_RT_ID_CALL
0x36`, subcmd INVITE/RINGING/ACCEPT/DECLINE/BUSY/CANCEL/HANGUP/MEDIA),
`call.c` (машина состояний, реестр сессий, conn_mgr, ring-timeout 45с,
watchdog no-traffic 20с, потеря соединения → end, события
CHAT_EVT_CALL_* 34–40), `DEBUG_CATEGORY_CALL 32`. `test_call` (2 инстанса,
happy path + decline + media-релей). Приём медиа — через `call_set_media_recv_cb`.
[+] **Headless API** — сделано. Команды control-сокета `call_start/accept/decline/
hangup` + события `call_incoming/ringing/accepted/ended/declined` (JSON);
аудио-сокет `call/call_headless.c` (TCP, бинарный фрейм
`[type:1][call_id:8][len:2][data]`, HELLO/FRAME), конфиг `call_audio_bind`.
Разрыв conn_mgr — отложенно 500мс (`CALL_TEARDOWN_DELAY_TB`) после ENDED,
чтобы HANGUP успел долететь; защита от stale-handle через conn-status.
Тесты: `test_call` (сигналинг+медиа), `test_call_headless` (полный e2e через
control+audio сокеты).
[+] **Аудио-контур (time-stretch в сервисном слое)** — сделано. `src/call/audio_jitter.cpp`
(SoundTouch, C API `ajb_*`: адаптивный джиттер-буфер ≤1000мс, target 60мс,
tempo 1.0→1.6x — догон live без потери данных при сетевой паузе),
`src/call/call_tones.c` (глитч при сталле >500мс + тон завершения),
`src/call/call_audio.c` (Opus 48kHz/20мс + jitter + тоны + pull API финального PCM).
`call.c`: `call_audio_ops` (on_media c seq/ts, get_stats) + событие
`CHAT_EVT_CALL_STATS` раз в 1с [rtt_ms, buffer_ms, tempo_x100].
Desktop `call_audio_engine.cpp` — только miniaudio I/O (получает финальный PCM).
Тест `test_audio_jitter` (ёмкость/догон/стационар). Демон остаётся C-only.
[+] **Android** — сделано. JNI (`utun_bridge_call_*`/`nativeCall*`), AudioRecord/Track
(CallAudioEngine), роутинг источника (CallAudioRouter: EARPIECE/SPEAKER/HEADSET,
BT SCO), CallScreen + IncomingCallDialog + RingtonePlayer + IncomingCallManager,
`libutun_lite/call_audio.c` (собственный аудио-контур Opus 48kHz/20мс).
[+] **Единый голосовой стек (voice stack) + SoundTouch на Android** — сделано.
Унификация аудио-контура звонка: `libutun_voice` (статическая C++-либа с
C-интерфейсом `call_audio.h` = SoundTouch + `voice_jitter.cpp` + `call_audio.c`
+ `call_tones.c`), собирается через `src/call/voice_sources.cmake` и линкуется
desktop/Android/headless-CLI. Джиттер-буфер держит КОДИРОВАННЫЕ (Opus) кадры,
декодирует только на pull (непосредственно перед воспроизведением) — память ~48x
меньше и uasync-поток разгружен. Android переведён с push-FIFO (Kotlin ArrayDeque)
на pull-модель (`nativeCallAudioPull`), своего джиттера больше нет → убрана
асимметрия задержки (desktop↔Android). Headless-аудио-сокет отдаёт готовый PCM
(декод Opus→PCM, без тактирования; клиент сам задаёт темп). Диагностика:
`CALL_STATS` расширен [dropped, underruns] (раз в 1с), счётчики headless.
Удалены `audio_jitter.cpp` и `libutun_lite/call_audio.c`. Тест `test_voice_jitter`
(ёмкость/догон/стационар). Android — `ANDROID_STL=c++_static`.
[ ] **DM-звонки** (fallback `group_id==0`) — не сделано: `call_is_online`/`call_start`
требуют реальную группу (`topo_groups_find`), для DM (group_id==0) маршрута нет.
## Звонок: две фазы исходящего вызова (индикация + гудки)
[+] **Две фазы вызова** — сделано. Исходящий звонок разбит на 2 фазы с индикацией
в окне вызова и разными гудками:
- фаза 1 «Пытаемся связаться…» (`OUTGOING` — INVITE/маршрут) — гудок «соединение»;
- фаза 2 «Звоним…» (`RINGING` — у пира идёт вызов) — длинные гудки (ringback).
Desktop: `CallWindow::Ringing` + `SoundManager` loop `call_connecting`/`call_ringing`
(MP3 в qrc), переключение по `onCallRinging`, остановка по accepted/ended/error;
добавлена обработка `GUI_EVT_CALL_ERROR` (40) — раньше при «пир офлайн» окно/гудок
зависали навсегда. Android: `CallRingback` (MediaPlayer loop из res/raw),
подписи `OUTGOING`/`RINGING` в CallScreen + ActiveCallBanner.
Звуковые события добавлены в SoundSettingsPage (call_connecting/call_ringing).
[+] **Надёжность отмены вызова (кодограмма + watchdog)** — сделано. Отмена вызова
(HANGUP/CANCEL) шлётся один раз, без ретрансмита, и при флапе маршрута (RST/epoch,
прямой TCP к мобиле не поднимается — только релей) терялась: вызываемый висел в
RINGING вечно и отвечал BUSY на все новые INVITE. Исправлено моделью «сердцебиения»:
- вызывающий шлёт кодограмму INVITE каждые 300мс весь период вызова (OUTGOING+RINGING);
- у вызываемого watchdog 2с — нет кодограммы → вызов сорван (`CALL_REASON_CANCEL`);
- завершающие кодограммы без ретрансмиссий (потерю покрывает watchdog);
- запоздалые кодограммы вызова при разговоре (CONNECTING/ACTIVE) игнорируются.
`CALL_INVITE_RETRY_TB=3000`, `CALL_RING_DEAD_TB=20000`, `CALL_NO_ROUTE_MAX_STRIKES=20`.
## Рефакторинг: per-instance chat/dm (однопоточный test_dm_e2e)
Сделано (глобальные синглтоны переведены на `UTUN_INSTANCE`):
- `chat_setting` → `inst->chat_settings` (`struct chat_setting_state`, state-level API для парсера конфига + instance-level для runtime).
- `chat_event` → `inst->chat_event_handler` (обработчик теперь получает `inst`).
- `chat_core` (`g_cc`) → `inst->chat_core` (`struct chat_core_ctx*`).
- `chat_sync` (`g_cs`) → `inst->chat_sync`.
- `member_sync` callback-списки (`g_props_cbks`/`g_apply_cbks`) → в `chat_core_ctx`.
- `chat_join` (`g_keys`) → в `chat_core_ctx`.
- `chat_msg` media-счётчики/backfill → в `chat_core_ctx`.
- `dm_core` (`g_dm`) → `inst->dm`; `dm_mailbox` (`g_mb`) → `inst->dm_mailbox`.
- `chat_headless_control` (`g_hc`) → `inst->headless`.
- `chat_whisper` — сигнатуры `init/available/trigger` принимают `inst`.
- API протащено через `inst` (chat_core_*/dm_*), обновлены call-сайты в src/ (headless, topo, media_delivery, auto_socket, config_parser).
- `test_dm_e2e.c` переписан на однопоточный: один `uasync`, три `utun_instance` (A,B,C), master state-machine, без fork. Фаза 2 (B offline) — `utun_instance_destroy(B1)` + пересоздание B2.
- Попутно исправлен 1-байтовый overflow в `dm_mailbox.c: mb_route_send` (`u_malloc(1+body_len)` → `1+1+body_len`).
Статус сборки: `make -C src` (libutun.a + бинарник utun) и `make -C tests` — OK.
### Осталось (по порядку)
[+] **test_dm_e2e: зависание в utun_instance_destroy(B1)** (фаза P_B_OFFLINE) — сделано.
Причин было несколько (не одна):
1. `lib/u_async.c`: если ближайший таймер уже истёк на входе в `uasync_poll`,
`get_next_timeout` возвращал {0,0}, `timeout_ms=-1` → `epoll_wait(-1)` блокировался
навсегда, а `process_timeouts` (вызывается только после epoll_wait) не запускался.
Фикс: `if (timeout_ms == -1 && heap не пуст) timeout_ms = 0`.
2. `routing_layer/etcp_router.c`: `router_send_conn` с group_id=0 (глобальная
маршрутизация DM/mailbox) не находил маршрут (`topo_groups_find(0)==NULL`).
Фикс: fallback на `instance_find_conn(inst, remote)` при group==NULL.
3. `dm/dm_core.c`: `dm_on_conn_status` PULL-ил только беседы с подключившимся пиром;
B2 подключается к storage (C), а беседа — с A. Фикс: на подъём любого соединения
PULL для всех бесед, чей пир не подключён напрямую.
4. `dm/dm_mailbox.c`: не было ACK после PULL_RESP → storage не чистил dm_mail.
Фикс: PULL_RESP теперь несёт sender, получатель шлёт MB_SUBCMD_ACK.
5. `utun_instance.c`: `dm_core_init` ставил deliver-cb до `dm_mailbox_init` →
mailbox не находился, deliver-cb не регистрировался. Фикс: mailbox init раньше core.
`test_dm_e2e` проходит (несколько прогонов), `test_dm` тоже.
[+] **chatgui-android: переезд на per-instance API** — сделано.
`instance_lite.c/h` (+`instance_lite_get_instance()`), `jni_bridge.c`, `standby.c`,
`headless_control.c`, `voice_recorder.c`, `attachment_sender.c`, `video_sender.c`,
`photo_sender.c` — все call-сайты переведены на `inst`. `libutun_lite` компилируется,
`jni_bridge.c` — синтакс-чисто. (headless-линковка сломана ПРЕДЫДУЩЕ — instance_lite.c
ссылается на jni_bridge-функции, не включённые в headless-сборку; отдельная задача.)
[+] **test_chat_join: segfault** (регресс рефакторинга) — сделано.
`chat_join` хранил `join_keys` в `chat_core_ctx` (CC(inst)), а тест использует chat_join
без chat_core → NULL-deref. Фикс: `join_keys` вынесен в `UTUN_INSTANCE.join_keys`
(у chat_join собственный init/destroy).
[ ] **BGP-гонка: дропнутый REQUEST_TABLE** (отдельная задача, как договорились).
**Место:** `topo_group_new_conn()` в `src/routing_layer/topo_group.c:571`. Дедуп-ветка
«conn already in senders_list» (строки 579–587) делает `return` БЕЗ повторной отправки
`topo_group_send_table_request()`.
**Зачем дедуп:** тот же `ETCP_CONN` стреляет `ETCP_CONN_STATUS_UP` дважды (UDP-линк,
затем TCP-линк). Повторная обработка задваивает `active_conn_count`, из-за чего
переподключение не стартует. Поэтому `return` в дедупе — правильный.
**В чём гонка:** REQUEST_TABLE — это то, что заставляет пира отдать свою таблицу
(NODEINFO/узлы группы). Сценарий «разнесённого старта»:
1. A↔C соединение поднято. A шлёт C `REQUEST_TABLE` для группы G (`topo_group_new_conn`
→ `topo_group_send_table_request`, `topo_group.c:602`).
2. У C группа G ещё **не создана** (создаётся позже, по мере загрузки каналов/синка).
C получает `REQUEST_TABLE`, но `topo_group_handle_request_table` не находит G →
дропает запрос.
3. C наконец создаёт G (`topo_groups_create_group` → обход connections →
`topo_group_new_conn(G, conn_A)` на стороне C) → C шлёт A свой `REQUEST_TABLE`.
A отвечает своей таблицей → **C узнаёт A**.
4. Но A больше не шлёт C `REQUEST_TABLE` → **A так и не узнаёт C** в группе G.
Итог: при разнесённом старте A не видит C в CHAT-группе (односторонняя видимость).
**Фикс (предполагаемый):** в дедуп-ветке не просто `return`, а предварительно
идемпотентно `topo_group_send_table_request(group, conn)`:
```c
if (((struct TOPO_GROUP_CONN_ITEM*)se->data)->conn == conn) {
topo_group_send_table_request(group, conn); /* повторно, идемпотентно */
DEBUG_INFO(..., "conn already in senders_list, re-request table (%s)", conn->log_name);
return;
}
```
`topo_group_send_table_request` не ведёт состояния — просто шлёт REQUEST_TABLE,
пир отвечает снимком таблицы (идемпотентно). Лишний дубль таблицы безвреден.
**Замечание:** в однопоточном `test_dm_e2e` (и `test_chat_join_e2e`) из-за
детерминированного порядка группа успевает создаться до прихода REQUEST_TABLE —
поэтому тест проходит. Реальная гонка остаётся для асинхронных сетей. Отдельный
воспроизводимый тест для этой гонки пока не написан.
[ ] **«packet undecryptable» на линках C** (вторичное). A, узнав B через BGP, через
`topo_group_connect` пытается поднять прямой A↔B линк (`seskey 4777737b`), но B не
настроен принимать A → crypto-шум каждые ~1с. Разобраться с авто-подключением
(не пытаться соединяться с узлами, для которых нет `[client]`/ncd-конфигурации,
либо обрабатывать неудачу без спама).
[+] **Обновить GUI call-сайты `tools/chatgui` (Qt, desktop)** — сделано.
- В `chat_core.h` вынесены trampoline-структуры из `.c`: `update_my_name_arg`,
`save_ui_state_arg`, `chat_setting_arg` (были в chat_profile.c/chat_core.c).
- `gui_bridge.h/impl`: `gui_bridge_set_inst()`/`gui_bridge_get_inst()` (+`g_inst`).
- `utun_node.cpp`: `gui_bridge_set_inst(m_instance)`, `chat_event_set_handler(m_instance,…)`,
`chat_core_sync_my_addresses(m_instance)`.
- Обновлены ~45 call-сайтов в 16 файлах (`mainwindow`, `messagelist`, `messagedelegate`,
`memberlistmodel`, `memberpropsdialog`, `accountlist`, `invitedialog`, `inviteby`,
`joindialog`, `settingsdialog`, `channelsettingsdialog`, `soundsettingspage`,
`audiodevicesettingspage`, `storagesettingspage`, `statuspage`, `connmonitorwindow`).
- Сборка `cmake --build . --target vibechat` — OK.
Попутно: `chat_sync_connect_from_invite` писал `cs->pending_*` на вызывающем (GUI) потоке —
перенёс в `cm_invite_trampoline` (uasync-поток), убрав гонку; заодно исправил утечку
`inv->addrs_data` и добавил null-check `cs` в трамплине.
[+] **Прогон всех тестов** — сделано. `make clean && make -j4` OK; все тесты проходят
(кроме `test_auto_socket_dynamic` — пропуск «requires root»). `test_dm_e2e` стабилен
в нескольких прогонах. В `test_dm_e2e` при teardown остаётся timer-leak (~68 узлов,
`router_no_route`), не влияет на результат — стоит разобрать отдельно.
[ ] **lwip/proxy: утечка pcb в FIN_WAIT_2** — `tcp_shutdown(pcb,0,1)` (half-close по FIN от
exit) не ставит `TF_RXCLOSED`, поэтому таймаут FIN_WAIT_2 в `tcp_slowtmr` не срабатывает:
если браузер никогда не пришлёт FIN, pcb навсегда остаётся в FIN_WAIT_2 (утечка, не crash).
Двойной free по TIME_WAIT уже закрыт (detach по локальному закрытию + guard abort).
## Открытые флаки
[+] **test_chat_join_e2e** — сделано. Причина: ложный критерий успеха джойнера J.
`wait_group_started()` проверял `senders_list` непуст в предположении «senders_list
непуст ⟺ J получил JOIN_READY». После коммита b483335c `tgi_to_channel_cb`
(topo_group_invite.c) при `NCD_EVENT_UP` стал вызывать `topo_group_new_conn` →
conn попадает в `senders_list` сразу после установки TCP/UDP-соединения, ДО
завершения хендшейка (JOIN_INFO_REQ→RESP→REQUEST→READY). J выходил «OK» раньше,
чем A получал JOIN_REQUEST → A не подписывал J → `wait_signed` на A/C падал
(`get_sign_rc=-1`). Фикс: критерий J — событие `CHAT_EVT_CONNECT_RESULT(result=0)`
(JOIN_READY), ловится через `chat_event_set_handler`; `wait_group_started` удалён.
Это также убрало гонку «C вышел раньше, чем J синкает свой рекорд» (C выходит сразу
после `wait_signed`, не дожидаясь merkle-синка J). 5 прогонов стабильно 3/3.
[ ] **test_etcp_reconnect** — флаки под параллельной нагрузкой `make check -j4`:
phase 4 (reconnect после server restart) таймаутит (`sent=480 recv=0`), при одиночном
запуске стабильно `TEST PASSED`. Наш код (chat_join/chat_sync) его не трогает.