diff --git a/doc/ble_etcp.txt b/doc/ble_etcp.txt index e5b209fb..53f7687a 100644 --- a/doc/ble_etcp.txt +++ b/doc/ble_etcp.txt @@ -1,12 +1,13 @@ BLE GATT → ETCP (chatgui desktop + Android) -Текущее состояние диагностики задержек (29.10.2026): -Write Without Response/Notify, HIGH и PHY2M включены. Исправлены готовность -BlueZ по ServicesResolved, использование старого ManufacturerData, входящий -HIGH Android и потолок DATA RTO=1s. Версия объявления повышена до 2, чтобы -не подключаться к старому DATA Write/Indicate сервису. Полная нагрузка обеих -аппаратных ролей пока не подтверждена: остаётся очередь между локальным -completion и HCI TX. Успех короткой пробы не означает устранения этой очереди. +Текущее состояние (29.10.2026): +Write Without Response/Notify, HIGH и PHY2M включены. BLE version3 добавляет +16-bit ID каждого DATA GATT-value, накопительный ACK по отдельной характеристике +не чаще15ms и окно12500байт на направление (фиксированные2Мбит/с ×50ms). +Локальный completion не возвращает кредит. Формат и таймауты — doc/ble_transport.txt. +Предыдущие исправления готовности BlueZ, старого ManufacturerData, incoming HIGH +и потолка DATA RTO сохранены. Ограничение очереди само по себе не считается +подтверждённым исправлением всей ETCP-нагрузки; аппаратные результаты ниже. Общий код: tools/cgathui_common/ble_etcp.c/h. Платформенные backend остаются в lib/ble и android_ble.c; Qt не участвует в протоколе. @@ -52,10 +53,14 @@ max_packet=1500 — размер непрозрачного пакета BLE, в TX limit=1 включает пакет в процессе фрагментации. У адаптера нет второй TX-очереди: send сразу передаёт ll_entry в ble_tx_put. Готовность читается через ble_tx_ready; после завершения целого пакета writable будит ETCP. При отказе владение не меняется. +Дополнительно ble_tx_ready учитывает ACK-кредит: после локальной отправки целого +пакета writable выдаётся, только если есть место в удалённом окне. При полном +окне ACK освобождает кредит и будит отправителя; INIT/keepalive проходят тот же лимит. RX использует ll_queue общего транспорта и включается packet_link потребителем. DATA передаётся через Write Without Response (client → server) и Notification (server → client). BLE send завершает только локальную отправку; доставку и повторы обеспечивает ETCP. INVITE остаётся отдельным Write Request/Indication каналом. +BLE ACK подтверждает приём GATT-value драйвером, не доставку целого ETCP-пакета. После BLE HELLO/ACCEPT первым целым кадром каждой стороны идёт 40 байт: 'V' 'C' 'E' 'T' 01 00 00 00 | 32 байта X25519 public key diff --git a/doc/ble_transport.txt b/doc/ble_transport.txt index be1924d5..03863513 100644 --- a/doc/ble_transport.txt +++ b/doc/ble_transport.txt @@ -5,7 +5,7 @@ BLE транспорт VibeChat (lib/ble) Независимая от Qt библиотека предоставляет manufacturer-броадкасты и адресные двусторонние GATT-сессии. Один локальный GATT-сервис обслуживает до 16 соединений. Броадкаст — повторяемое ОС объявление, а не гарантированно доставляемая датаграмма. -Общий codec ble_identity.h: VC (2 байта), версия 2 (1), node_id BE64 (8), +Общий codec ble_identity.h: VC (2 байта), версия 3 (1), node_id BE64 (8), имя UTF-8 (0..13). Имя сокращается по границе символа. Всего 11..24 байта. Приглашения и автоматический выбор соседей реализованы в tools/cgathui_common (doc/ble_etcp.txt); транспорт не запускает JOIN/ETCP. Node ID передаётся в HELLO для сопоставления сессий; подлинность он не доказывает. @@ -61,6 +61,13 @@ RX при заполнении отбрасывает целый новый ка если он присутствует. Это не Ethernet payload MTU 1500 (которому нужно >=1514). На link не больше одной незавершённой GATT-передачи. Она хранит свой fragment до завершения ОС. Очередь ядра/Qt не используется как неограниченный TX-буфер. +DATA дополнительно ограничен неподтверждёнными GATT-values: фиксированные2Мбит/с +×50ms/8 =12500байт на направление, включая flow/framing headers. Локальный sent +не возвращает этот кредит: его освобождает накопительный ACK удалённого драйвера. +При заполнении ble_tx_ready=false; ACK будит writable после появления места. +Текущий кадр может остановиться посреди фрагментации, INVITE и ACK продолжают работать. +Предел12500 — расчётный размер по фиксированной скорости, не гарантия фактических50ms +при медленном радио. Сверх окна остаётся только ограниченная собственная TX-очередь. Переподключение не переносит старые кадры и не повторяет неоднозначные отправки. Wire protocol @@ -69,31 +76,58 @@ UUID service: f61a9020-4abc-45ac-98e1-761df4c13811 UUID RX: f61a9021-4abc-45ac-98e1-761df4c13811 (Write Without Response) UUID TX: f61a9022-4abc-45ac-98e1-761df4c13811 (Notify) UUID INVITE: f61a9023-4abc-45ac-98e1-761df4c13811 (Write + Indicate) +UUID ACK: f61a9024-4abc-45ac-98e1-761df4c13811 (Write + Indicate) RX/TX названы относительно сервера; API одинаков для обеих ролей. -Версия объявления 2 означает DATA Write Without Response/Notify; объявления -версии 1 отклоняются до GATT-подключения. Desktop и Android должны использовать +Версия объявления3 означает DATA Write Without Response/Notify с16-bit flow ID +и обязательной ACK-характеристикой; версии1/2 отклоняются до GATT-подключения. +Desktop и Android должны использовать новую версию вместе. Scan prefix Android приходит из C codec через JNI start, номер версии в Kotlin не дублируется. Это отдельный протокол от BLE_EXAMPLE и tools/ble-test-android; автоматическая совместимость с ними не заявляется. Все многобайтные целые big-endian, без выравнивания C-структур. +Каждое DATA GATT-value начинается с flow_id(2), затем одно из тел ниже. +INVITE содержит только тело FRAME без flow_id; ACK содержит только last_flow_id(2). HELLO: [type=1:1][source_node:8][target_node:8], Central -> Peripheral. ACCEPT: [type=2:1][source_node:8][target_node:8], Peripheral -> Central. DATA: [type=3:1][frame_id:2][total_len:2][offset:2][payload:1..]. HELLO/ACCEPT идут только по DATA, сборка DATA и INVITE независима. frame_id увеличивается modulo 65536 отдельно для каждого направления и канала. -Планировщик чередует фрагменты DATA/INVITE; HELLO имеет приоритет. +Планировщик чередует фрагменты DATA/INVITE; готовый ACK имеет высший приоритет, +затем HELLO/ACCEPT. До HELLO обязательны подписки DATA/INVITE/ACK с обеих сторон. Размер значения <= min(ATT_MTU-3,512), минимальный ATT MTU=23. +Полезный DATA-фрагмент:11байт при MTU23,235 при247,503 при517; HELLO/ACCEPT занимают19. Нулевой offset начинает новый кадр и заменяет незавершённую предыдущую сборку. Далее обязательны тот же frame_id/total_len и offset==число принятых байтов. Пропуск, неверная длина, новое начало или 15-секундный таймаут сбрасывают сборку; -в RX помещается только целый кадр. Собственных ACK/ретрансляций/окна протокола нет. +в RX помещается только целый кадр. Повторы целых кадров выполняет потребитель. DATA, включая HELLO/ACCEPT и начальный обмен ключами, использует Write Command и Notification без ATT-подтверждений. Подтверждения/повторы данных выполняет ETCP; потеря начального обмена завершает попытку по существующему дедлайну подключения. INVITE сохраняет Write Request и Indication. Завершение локального send не означает чтения кадра приложением и не защищает от заполнения RX. +ACK и управление окном +---------------------- +flow_id начинается с1, увеличивается modulo65536 для каждого DATA GATT-value, +включая HELLO/ACCEPT, bootstrap, ETCP control и DATA. ACK — последний полученный +flow_id; при пропуске последующий ID освобождает кредит предыдущих отправок, +но НЕ утверждает их доставку приложению. Сборщик/ETCP обнаруживают потери отдельно. +Дубликат/старый DATA не выдаётся повторно, текущий ACK может быть повторён. +Кольцо хранит только размеры неподтверждённых GATT-values, без копии payload. +В окне не более1250 минимальных DATA-values по10байт; это меньше половины16-bit +пространства ID. Modulo-разность ACK однозначно проверяется по активному окну. +Старый/повторный ACK не освобождает кредит, ACK вперёд за отправленное окно закрывает +соединение с WARN. При reconnect все flow ID/кредит/таймеры создаются заново. + +ACK отправляется отдельным uasync-таймером, не polling-таймером100ms. Поступившие +ID объединяются до последнего; между принятыми backend ACK не менее15000us. +Один ACK в процессе отправки и один объединённый ожидающий; очереди ACK нет. +ACK не расходует DATA-кредит и не требует встречного DATA. Его Write Request/ +Indication подтверждается ATT; ошибка/зависание завершают соединение с диагностикой. +15s без продвижения ACK закрывают link даже при успешных локальных completions. +Дубликаты ACK этот срок не продлевают. Close/stop отменяют ACK-таймер. + Linux ----- BlueZ D-Bus, клиент sd-bus из libsystemd; зависимость от Qt отсутствует. @@ -106,11 +140,11 @@ instance может иметь другой random address). Нехватка р Входящий WriteValue содержит device; серверный AcquireNotify(device,mtu) получает отдельный SOCK_SEQPACKET socketpair для каждого клиента и канала. DATA fd принимает notifications: успешная запись завершает локальный send без ожидания ATT ACK. -INVITE fd передаёт indications; байт 1 в обратную сторону подтверждает ATT completion. +INVITE/ACK fd передают indications; байт1 в обратную сторону подтверждает ATT completion. EAGAIN сохраняет фрагмент и ждёт writable; синхронных callbacks send нет. Value/Confirm без адреса клиента для серверной передачи не используются. -Выходящий клиент: DATA WriteValue(type=command), INVITE WriteValue(type=request), -StartNotify для TX и INVITE; открытие после обеих подписок. +Выходящий клиент: DATA WriteValue(type=command), INVITE/ACK WriteValue(type=request), +StartNotify для TX, INVITE и ACK; открытие после всех трёх подписок. Публичный Device1 D-Bus API не задаёт per-link PHY/connection interval: согласованием управляют ОС и peer; Android запрашивает 2M также в серверной роли. Не вызывается глобальный Device1.Disconnect: ACL может обслуживать другие сервисы. @@ -120,8 +154,8 @@ Windows ------- WinRT BluetoothLEAdvertisementWatcher/Publisher и GattServiceProvider. GATT-клиент использует WriteValueWithResultAsync, сервер — адресный -NotifyValueAsync(value, subscribedClient): DATA — notify-only, INVITE — indicate-only. -DATA пишет с WriteWithoutResponse и подписывается на Notify; INVITE сохраняет +NotifyValueAsync(value, subscribedClient): DATA — notify-only, INVITE/ACK — indicate-only. +DATA пишет с WriteWithoutResponse и подписывается на Notify; INVITE/ACK сохраняют WriteWithResponse/Indicate. Сервер отвечает Respond только на WriteWithResponse. После открытия исходящего GATT при наличии IBluetoothLEDevice6 (Windows 11) запрашивается ThroughputOptimized. Request хранится до close линка и закрывается @@ -179,6 +213,9 @@ PHY_2M_supported — возможность адаптера, НЕ измере TX/RX_Bps полезных байтов BLE-кадров (оба канала вместе, до вычета заголовков ETCP), число кадров/фрагментов/отбросов, среднее/максимальное локальное ожидание GATT, размеры DATA/INVITE очередей и возраст pending. Локальный TX не равен доставке. +Отдельная сводка flow показывает flight bytes/packets, peak_bytes, ID/ACK и число +ACK TX/RX. INFO печатает фиксированную скорость/окно/интервал/разрядность ID. +Wire TRACE категории debug содержит каждый flow TX, ACK TX/RX и освобождённый кредит. Повторные радиообъявления не засоряют DEBUG. Desktop GUI @@ -206,7 +243,7 @@ eventfd немедленно будит uasync при новом событии; Переполнение mailbox останавливает radio с ошибкой. На соединение не более одной ожидающей отправки; сервер notifications/indications обслуживает адресно, по очереди. DATA использует WRITE_TYPE_NO_RESPONSE, CCC ENABLE_NOTIFICATION_VALUE и confirm=false; -INVITE — WRITE_TYPE_DEFAULT, ENABLE_INDICATION_VALUE и confirm=true. Оба направления +INVITE/ACK — WRITE_TYPE_DEFAULT, ENABLE_INDICATION_VALUE и confirm=true. Оба направления ждут локальный callback готовности стека, даже когда ATT ACK отсутствует. Политика входящих ble_set_incoming закрывает только входящие сессии. Настройки используют DataStore; Bluetooth off останавливает только радио приложения.