Document BLE v3 ACK framing and bounded remote credit

This commit is contained in:
evgeny committed 2026-10-29 22:26:34 +01:00
1 parent 6338a2aae1
commit c83b30aa0b
2 files changed
+60 -18

No files matched your search

+12 -7
View File
@@ -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
+48 -11
View File
@@ -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 останавливает только радио приложения.