Назад к блогу
Transport19 min read

Дизайн транспорта Wi-Fi Aware — обнаружение соседей и data paths

В этой статье объясняется, как PlainApp использует Wi-Fi Aware (NAN — Neighbor Awareness Networking) как средний уровень своей цепочки fallback'а peer-транспорта, между LAN (HTTPS в одной подсети) и BLE (GATT RPC последней инстанции). Wi-Fi Aware — это то, что позволяет двум устройствам PlainApp общаться, когда они в разных SSID, guest vs IoT VLANs, или вообще без Wi-Fi-инфраструктуры — без необходимости в IP-адресе от DHCP-сервера.

Статья описывает только для Android жизненный цикл Aware-сессии, модель обнаружения publish / subscribe, двухфазное рукопожатие с разделением ролей, синхронизирующее requestNetwork на обеих сторонах в ~500 мс окне фреймворка, пул per-peer линков с очисткой простаивающих, трюк с IPv6 + кастомным DNS, позволяющий одному OkHttp-клиенту обслуживать и LAN, и Aware, и prewarmer, запускающий Aware пира через BLE.

Более широкая цепочка fallback'а — см. Chat Architecture. BLE-транспорт, вступающий при недоступности Aware, — см. BLE Transport. О том, как устанавливается общий ключ ChaCha20, переиспользуемый как Aware PMK, — см. Pairing Flow.

Содержание

Почему Wi-Fi Aware? {#why-wi-fi-aware}

Wi-Fi Aware (IEEE 802.11bc, ранее NAN — Neighbor Awareness Networking) — сертификация Wi-Fi Alliance, позволяющая двум устройствам обнаруживать друг друга и обмениваться данными без какой-либо Wi-Fi-инфраструктуры — без AP, без роутера, без DHCP. PlainApp использует его в двух сценариях, которые не покрывает LAN:

  • Разные SSID / VLAN. Телефон в гостевой сети и ноутбук в IoT VLAN оба «онлайн» по Wi-Fi, но не могут достучаться до IP друг друга. Aware создаёт прямой device-to-device data path, полностью обходящий инфраструктуру.
  • Инфраструктуры нет вообще. Два устройства в глуши с включённым Wi-Fi, но без AP, всё ещё могут общаться. (BLE это тоже покрывает, но Aware намного быстрее — ~10 мс round trip против секунд, и MB/s против десятков KB/s.)

Diagram 1
1

Платформенные ограничения

Wi-Fi Aware в PlainApp — только Android:

  • Android 13 (API 33) — минимум; перегрузки WifiAwareNetworkSpecifier.Builder с setPort() и setPmk(), от которых зависит PlainApp, требуют isTPlus().
  • iOS не предоставляет Wi-Fi Aware сторонним приложениям. iOS PlainApp падает напрямую с LAN на BLE; объект WifiAwareTransport даже не компилируется в iOS-таргет (@RequiresApi(Build.VERSION_CODES.S) + androidMain source set).

Именно поэтому PeerTransportRouter.buildList вызывает createWifiAwareTransport() — фабрику, возвращающую null на iOS.

Где Aware находится в цепочке fallback'а {#where-aware-sits-in-the-fallback-chain}

PeerTransportRouter PlainApp — упорядоченный список. На каждый вызов send или downloadFile он обходит список и пробует каждый транспорт, пока один не успеет; сбои каскадом спускаются ниже.

Diagram 2
2

Почему Aware «в середине», а не «первый»?

Потому что LAN почти всегда быстрее, когда доступен. Один Wi-Fi hop внутри одной подсети через AP — это один обмен 802.11-кадрами; Aware data path добавляет NDP setup (~5 с при первом использовании) плюс второй контекст Wi-Fi-радио для device-to-device линка. Если оба достижимы, LAN выигрывает по задержке и пропускной способности.

И наоборот, BLE всегда медленнее — но он работает, когда оба устройства сопряжены. Aware посередине: быстрее BLE, медленнее LAN, и доступен только на Android 13+ с включённым Wi-Fi.

Жизненный цикл сессии: Attach → Publish + Subscribe {#session-lifecycle-attach--publish--subscribe}

Wi-Fi Aware сессия — процесс-глобальная. На устройстве ровно одна WifiAwareSession; внутри неё PlainApp запускает одну publish-сессию (чтобы пиры могли нас обнаружить) и одну subscribe-сессию (чтобы мы могли обнаруживать пиров). Обе стартуют сразу, как только AwareSession.start() завершает колбэк attach.

Diagram 3
3

Зачем publish И subscribe на одном устройстве?

Модель обнаружения Wi-Fi Aware асимметрична: publisher рекламирует сервис, subscriber его сканирует. Чтобы сделать обнаружение симметричным (оба устройства обнаруживают друг друга), PlainApp делает обе роли сразу. Без этого устройство A должно было бы заранее знать, является ли оно publisher или subscriber для данного пира — но роли пиров определяются позже сравнением clientId (см. Обнаружение и назначение ролей).

Одновременный publish и subscribe означает, что каждое устройство видит onServiceDiscovered другого (как subscriber) И получает hello-сообщения другого (как publisher) — оба направления рукопожатия всегда доступны.

Авторестарт при завершении

Некоторые Android-варианты (особенно MIUI) убивают длительные Aware-сессии для экономии батареи. PlainApp обрабатывает это в колбэках onSessionTerminated: он зануляет завершённую сессию и немедленно снова вызывает publishOwnService / subscribeOwnService на всё ещё подключённой WifiAwareSession. Сама attach-сессия не теряется — только publish/subscribe discovery-сессия. Peer handle'ы до завершения становятся устаревшими, поэтому awaitPeerHandle проверяет метку времени discoveredAt и отбрасывает handle'ы старше 30 с.

Обнаружение и назначение ролей {#discovery--role-assignment}

Протокол Wi-Fi Aware data-path требует одну сторону в роли publisher (server) и другую в роли subscriber (client). Обе стороны не могут одновременно быть инициаторами — фреймворк отклоняет запросы без соответствующей counterpart'и.

PlainApp назначает роли детерминированно per peer простым лексикографическим сравнением clientId:

Diagram 4
4

Почему детерминированно, а не через согласование?

Согласованный подход (например, «меньший MAC — сервер») потребовал бы лишнего обмена сообщениями. Лексикографическое сравнение идемпотентно, симметрично и stateless: оба устройства вычисляют одну и ту же роль для одной пары без всякого общения. clientId — 13-символьный короткий UUID, поэтому совпадения (clientId == peer.id) случаются только при сравнении пира с самим собой — что никогда не доходит до транспорта.

Роль определяет далее две вещи:

  1. Кто ведёт цикл повторных попыток. Только клиент повторяет requestNetwork; сервер делает ровно одну попытку на каждый полученный hello. Это критично для окна 500 мс (следующий раздел).
  2. Кто задаёт порт. Publisher вызывает setPort(httpsPort), поскольку именно он принимает входящие соединения на порту своего HTTPS-сервера. Subscriber порт не задаёт — он узнаёт порт пира из WifiAwareNetworkInfo после установки data path.

Двухфазное рукопожатие (hello + ready) {#the-two-phase-handshake-hello--ready}

Самая сложная часть настройки Wi-Fi Aware data-path — тайминг. Фреймворк требует, чтобы обе стороны вызвали connectivityManager.requestNetwork в течение примерно 500 мс друг от друга — если одна сторона вызовет его до того, как другая зарегистрирует соответствующий запрос, фреймворк немедленно отклоняет его onUnavailable ("releaseRequestAsUnfulfillableByAnyFactory").

PlainApp решает это двухсообщенийным прикладным рукопожатием, поверх Aware L2 message-канала (тот же sendMessage API, что используется в onServiceDiscovered):

Diagram 5
5

Почему два сообщения (hello + ready), а не одно?

Только hello недостаточно из-за асимметрии направлений. Subscriber может отправить hello сразу, как обнаружит publisher (в onServiceDiscovered), но publisher не может запустить requestNetwork, пока не получит PeerHandle subscriber'а, который он узнаёт только получив hello. Так что hello служит двум целям:

  1. Доставляет PeerHandle subscriber'а publisher'у. Publisher нужен он, чтобы построить WifiAwareNetworkSpecifier.
  2. Сигнализирует намерение подключиться. Получение hello говорит publisher'у «subscriber сейчас вызовет requestNetwork, поэтому мне тоже надо».

Квитанция ready существует для противоположного направления — сказать subscriber'у «publisher зарегистрировал свой requestNetwork». Без неё requestNetwork subscriber'а мог бы обогнать publisher'а и быть отвергнутым фреймворком. Квитанция readyнеблокирующий сигнал: subscriber не ждёт его перед вызовом requestNetwork (это добавило бы round trip), но если она приходит, пока subscriber в состоянии IDLE (между попытками), он может немедленно повторить, не дожидаясь паузы RETRY_DELAY_MS.

Асимметрия цикла повторов

Это самая тонкая часть дизайна. Повторяет только subscriber. Publisher делает ровно одну попытку requestNetwork на каждый полученный hello. Это связано с тем, что:

  • Если бы обе стороны повторяли независимо, их циклы повторов разошлись бы по фазе (разные delay() длительности, разные GC-паузы), и два вызова requestNetwork редко совпали бы в окне 500 мс.
  • Цикл повторов subscriber'а отправляет свежий hello на каждой попытке, что повторно триггерит buildLink publisher'а через publishHelloListeners. Это гарантирует, что requestNetwork publisher'а всегда следует за hello с отставанием ~50 мс, хорошо внутри окна 500 мс.

Подробно это задокументировано в AwarePeerLink.build.

NDP requestNetwork — окно 500 мс {#ndp-requestnetwork--the-500-ms-window}

Вызов requestNetwork — самая чувствительная ко времени операция в Aware транспорте. Вот что происходит на каждой стороне:

Diagram 6
6

Что означает колбэк onUnavailable

onUnavailable срабатывает, когда фреймворк отклоняет requestNetwork, не найдя соответствующего peer-запроса. Сам PeerHandle всё ещё валиден — неудачна только NDP (Neighbor Discovery Protocol)-связка, поскольку другая сторона ещё не зарегистрировалась. PlainApp намеренно не вызывает session.invalidatePeerHandle в этом случае, поскольку инвалидация handle'а отбросила бы единственный сигнал, что onServiceDiscovered вообще был вызван (он срабатывает один раз за пир за время жизни subscribe-сессии). Сохранив handle, повторная попытка может переиспользовать его, не ожидая свежего обнаружения.

То же относится к publisher-стороне handle из onMessageReceived — publisher хранит запись publishPeerHandles[fromCid] между неудачными попытками, поэтому следующий hello subscriber'а переиспользует закэшированный handle, а не теряется.

Каждый сопряжённый пир получает собственный объект AwarePeerLink, принадлежащий процесс-глобальному AwareLinkPool. Пул обрабатывает события обнаружения, переиспользование линка и выселение простаивающих.

Diagram 7
7

Почему не авто-сборка при обнаружении?

Пул явно не строит линк при срабатывании onServiceDiscovered. Это критическое решение: в шумной кофейне может быть 100 устройств PlainApp, все публикующие сервис «plain-peer». Если бы каждое обнаружение триггерило requestNetwork, фреймворк был бы затоплен попытками NDP setup, а Wi-Fi-радио было бы насыщено.

Вместо этого пул только записывает PeerHandle и ждёт одного из:

  1. Локальный пользователь отправил сообщениеWifiAwareTransport.sendpool.buildLink(peer) (триггер со стороны отправителя).
  2. Удалённый пир прислал helloonPublishHelloReceivedbuildLink(peer) (триггер со стороны приёмника).
  3. Удалённый пир прислал readyonSubscribeReadyReceivedbuildLink(peer) (триггер со стороны приёмника).

Таким образом, линки строятся только для пиров, с которыми пользователь реально обменивается сообщениями, — а не для каждого PlainApp-устройства в зоне действия радио.

Очистка простаивающих

Каждые 10 секунд пул обходит все линки и закрывает те, чей lastActiveAt старше 60 секунд. Каждый send и downloadFile вызывает link.touch() для обновления метки. Это возвращает контекст Wi-Fi-радио и пул OkHttp-соединений для пиров, с которыми пользователь перестал общаться, — что важно, поскольку Android ограничивает число одновременных Aware data paths примерно до 4–10 (зависит от устройства).

IPv6-адресация и DNS-трюк plain-aware-peer {#ipv6-addressing--the-plain-aware-peer-dns-trick}

Wi-Fi Aware data paths используют только link-local IPv6. IPv4 нет, DNS- сервера нет, DHCP нет. IPv6-адрес пира доставляется через поле WifiAwareNetworkInfo.peerIpv6Addr в onCapabilitiesChanged — адрес fe80::..., осмысленный только на сетевом интерфейсе Aware.

PlainApp должен отправлять HTTPS-запросы на этот адрес, но парсинг URL https:// в OkHttp отказывается принимать сырые IPv6-литералы в hostname (https://[fe80::abcd]:8443/ работает, но маршрутизировать через кастомный Dns-резолвер чище). Трюк:

Diagram 8
8

Зачем сентинельный hostname?

Альтернатива — передача IPv6-литерала напрямую в URL — потребовала бы, чтобы каждая точка вызова знала о link-local-адресе. Использование сентинельного hostname делает конструирование URL одинаковым для LAN и Aware: оба производят валидный https://<host>:<port>/peer_graphql URL, который OkHttp может распарсить. Единственное отличие — Dns-реализация, привязанная к клиенту: LAN использует системный DNS, Aware использует awareDns(peerIpv6), возвращающий закэшированный link-local-адрес для сентинельного hostname и проваливающийся к Dns.SYSTEM для всего остального.

Зачем network.socketFactory?

Объект Network Android представляет конкретный сетевой интерфейс (в данном случае Aware data path). Вызывая network.socketFactory и передавая его в конфиг socketFactory OkHttp, мы форсим создание всех TCP-сокетов на интерфейсе Aware — не на дефолтном Wi-Fi или сотовом интерфейсе. Без этого ОС маршрутизирует запрос через сеть по умолчанию, где link-local IPv6 недостижим, и запрос упадёт с ENETUNREACH.

Криптография: вывод PMK и переиспользование ChaCha20 {#cryptography-pmk-derivation--chacha20-reuse}

Wi-Fi Aware поддерживает опциональный PMK (Pairwise Master Key) для data path. Когда он установлен, сам L2-линк шифруется этим PMK — Wi-Fi-радио обрабатывает шифрование, прикладная криптография не нужна.

PlainApp выводит PMK из того же общего ключа ChaCha20, который LanTransport и BleTransport используют для прикладного шифрования:

Diagram 9
9

Зачем усекать до 32 байт?

Wi-Fi Aware PMK должен быть ровно 32 байта (256 бит). Общий ключ ChaCha20 из сопряжения тоже 32 байта в обычном случае, поэтому ветка raw.size == 32 — общий путь. Fallback с усечением/паддингом обрабатывает (теоретический) случай, когда ключ сохранён короче — паддинг нулями до 32 байт это оборонительная мера, не что-то, что случается на практике с корректно сопряжёнными пирами.

Подписанный конверт идентичен LAN

Поскольку createCryptoHttpClient — та же фабрика, что использует LanTransport, L7-криптография на Aware байт-в-байт идентична LAN. Серверный PeerGraphQLService не знает (и не заботится), какой транспорт доставил запрос — он просто видит подписанную, зашифрованную GraphQL-полезную нагрузку и расшифровывает её общим ключом пира. Это принцип «одна кодовая база, много транспортов», задокументированный в Chat Architecture.

Путь отправки сообщения (end-to-end) {#message-send-path-end-to-end}

Сводя всё вместе — что происходит при отправке сообщения чата через Wi-Fi Aware:

Diagram 10
10

Примечательные проектные решения

  • Переиспользование соединения. В отличие от BleTransport, разрывающего GATT-соединение после каждого запроса, WifiAwareTransport переиспользует Aware data path для стольких запросов, сколько пользователь делает в пределах 60-секундного окна простоя. Первый запрос оплачивает ~400 мс рукопожатие; последующие — ~10 мс round trip.
  • Та же криптография, что у LAN. ChaCha20-интерсептор и подписанный конверт байт-идентичны LAN. PeerGraphQLService пира не знает, какой транспорт доставил запрос.
  • Без preemption при сбое линка. Если buildLink падает, транспорт выбрасывает TransportUnavailable, и маршрутизатор проваливается к BLE. Повторных попыток внутри send нет — AwarePeerLink.build уже выполняет собственный внутренний цикл повторов (с MAX_BUILD_ATTEMPTS = 1 на клиенте, больше, если prewarmer «прогрел» обе стороны).

Путь загрузки файла (end-to-end) {#file-download-path-end-to-end}

Загрузки файлов через Aware переиспользуют тот же data path, что и сообщения чата, но используют отдельный OkHttp-клиент, настроенный на стриминг больших файлов:

Diagram 11
11

Зачем отдельный клиент для загрузок?

Чат-клиент (AwareHttpClientFactory.build) имеет 30-секундный requestTimeoutMillis — уместно для GraphQL-мутаций, но катастрофично для 100 MB загрузки файла. Клиент загрузок (buildFileDownload) ставит:

  • connectTimeoutMillis = 10_000 (дольше 5 с чата, более толерантен к медленному первому пакету на свежем data path)
  • readTimeout = 120 s на чтение (vs неявный дефолт 10 с)
  • requestTimeoutMillis = 120_000 (2 минуты — достаточно для большинства файлов)
  • retryOnConnectionFailure(true) — упавшее посреди загрузки чтение повторяется, а не валит всю передачу

Также он опускает ChaCha20-интерсептор. Endpoint /fs отдаёт сырые байты файла (не подписанный GraphQL-конверт), а L2 PMK (когда есть) уже шифрует радио-линк. Двойное шифрование 50 MB видео ChaCha20 в ПО потратило бы CPU и замедлило передачу.

Стриминг, не буферизация

Как и BLE-путь, загрузки Aware стримят файл через ByteReadChannel — файл пишется во временный файл по мере поступления байт, не буферизуется в памяти. PeerFileDownloader читает чанки 8 KB и эмитит события прогресса каждую секунду. Тот же конвейер DownloadedResponse / PeerFileDownloader / DownloadQueue переиспользуется между всеми транспортами — транспортно-специфичен только источник channel.

Prewarming: запуск Aware через BLE {#prewarming-ble-triggered-aware-startup}

Самая большая видимая пользователю задержка в Aware-пути — первое рукопожатие — если обе стороны ещё не запустили Aware, первое сообщение пользователя должно ждать:

  1. Local Aware session attach (~1 с)
  2. Local publish + subscribe start (~1 с)
  3. Aware-старт удалённого пира (~2 с через BLE)
  4. Взаимное обнаружение (~1 с)
  5. NDP-рукопожатие (~400 мс)

Это ~5 секунд до отправки первого байта. Чтобы скрыть эту задержку, PeerTransportPrewarmer запускается при входе на ChatPage и триггерит Aware-старт удалённого пира через BLE:

Diagram 12
12

Двойная роль BLE

BLE здесь служит двум целям:

  1. Прочитать текущее Aware-состояние пира (дёшево, без GATT-подключения — serviceData byte0 scan-response несёт Aware-флаги).
  2. Триггернуть пир запустить Aware, если он поддерживает, но сейчас не запущен. Это идёт через обычный путь BleTransport.send — мутация GraphQL startAware, зашифрованная общим ключом ChaCha20, доставленная через GATT RPC на endpoint /peer_graphql пира.

Это одно из немногих мест, где транспорты сотрудничают, а не просто проваливаются: BLE используется, чтобы превентивно апгрейдить сессию до более быстрого Aware-транспорта, прежде чем пользователь вообще заметит.

Зачем оптимистичный setAwareRunning(true)?

Мутация startAware возвращает успех, как только резолвер удалённого пира вызывает WifiAwareTransport.start() — но Aware-сессия ещё не присоединена (onAttached срабатывает асинхронно). PlainApp помечает пир как awareRunning = true оптимистично, потому что:

  • Если она действительно стартовала, следующий send использует Aware (быстро).
  • Если нет (например, Wi-Fi пира выключен), следующий send упадёт в buildLink с TransportUnavailable и естественно откатится к BLE.
  • Цена ложного позитива — один ~5-секундный тайм-аут, не постоянный блок — PeerCircuitBreaker записывает сбой, но не открывает BLE-ветвь (BLE открывается только по собственным сбоям).

Зачем троттлинг 30 с?

PeerTransportPrewarmer.prewarm(peerId) записывает метку времени per peer и отказывается перезапускаться в течение 30 с. Это потому, что пользователь часто ходит туда-сюда между списком чатов и страницей чата — без троттлинга каждая навигация триггерила бы BLE-скан + startAware-мутацию, высаживая батарею и спамя BLE-радио. Окно 30 с достаточно короткое, чтобы поймать пира, только что ставшего онлайн (например, пользователь открыл приложение на удалённом устройстве), но достаточно длинное, чтобы избежать спуртовых перезапусков.

Отказы и флаг fast-skip {#failure-modes--the-fast-skip-flag}

У Aware больше режимов отказа, чем у любого другого транспорта. Флаг fast-skip isAwareRunning — единственная самая важная оптимизация во всём модуле: без него каждый send тратил бы 10 с на тайм-аут buildLink перед откатом к BLE.

Diagram 13
13

Флаг isAwareRunning — краеугольный камень

Без этого единственного булева флага каждый Aware-send либо:

  • Всегда пытался бы buildLink → 10-секундный тайм-аут на каждом send пиру, чей Aware не запущен.
  • Всегда пропускал бы Aware → никогда не использовал бы его, даже когда обе стороны его запускают.

Флаг обновляется из двух источников, в порядке авторитетности:

  1. BLE scan response (дёшево, без GATT-подключения) — ставится PeerTransportPrewarmer.refreshAwareFlagFromScan. Пир рекламирует своё Aware-состояние в 9-байтной полезной нагрузке serviceData (byte0 битполе).
  2. GATT DISCOVER reply (авторитетно) — ставится PairingTransport.scanAndDiscover при полном обнаружении. Перезаписывает scan-подсказку.

Когда false, WifiAwareTransport.send и downloadFile выбрасывают TransportUnavailable немедленно — без сканирования, без рукопожатия, без тайм-аута. Маршрутизатор проваливается к BLE за микросекунды.

Справочник ключевых констант {#key-constants-reference}

КонстантаЗначениеГдеНазначение
AwareSession.SERVICE_NAME"plain-peer"ОбнаружениеИмя сервиса, публикуемое и подписываемое каждым устройством PlainApp
AwareSession.PEER_HANDLE_MAX_AGE_MS30 000Кэш PeerHandleОтбрасывать устаревшие handle'ы (publish-сессия пира могла быть перезапущена)
AwareSession.READY_TIMEOUT_MS15 000РукопожатиеОжидание subscriber'ом квитанции ready publisher'а
AwareSession.MSG_HELLO0РукопожатиеID сообщения Subscriber → Publisher
AwareSession.MSG_READY1РукопожатиеID сообщения Publisher → Subscriber
AwarePeerLink.MAX_BUILD_ATTEMPTS1Рукопожатие (только клиент)Одна попытка — было 3, теперь 1, потому что prewarmer прогревает обе стороны
AwarePeerLink.ATTEMPT_TIMEOUT_MS5 000РукопожатиеТайм-аут на попытку — было 10 с, вдвое уменьшено для ускорения fallback
AwarePeerLink.RETRY_DELAY_MS500РукопожатиеЗадержка между попытками (только клиент)
AwarePeerLink.REQUEST_TIMEOUT_MS30 000NDPТайм-аут connectivityManager.requestNetwork
AwareLinkPool.IDLE_TIMEOUT_MS60 000Очистка пулаЗакрывать простаивающие линки после 60 с неактивности
AwareLinkPool.IDLE_SWEEP_INTERVAL_MS10 000Очистка пулаИнтервал очистки
AwareHttpClientFactory.AWARE_HOST"plain-aware-peer"DNSСентинельный hostname, резолвящийся в IPv6 пира через кастомный Dns
build чат-клиентconnectTimeout 5 с, requestTimeout 30 с, ChaCha20-интерсептор
buildFileDownloadconnectTimeout 10 с, readTimeout 120 с, requestTimeout 120 с, без крипто
PeerTransportPrewarmer.PREWARM_TTL_MS30 000PrewarmТроттлинг per peer
PeerTransportPrewarmer.BLE_SCAN_TIMEOUT_MS15 000PrewarmТайм-аут BLE-сканирования для refreshAwareFlagFromScan
PeerCircuitBreaker.WINDOW_MS30 000Circuit breakerДлительность открытия после порога
PeerCircuitBreaker.MAX_FAILURES2Circuit breakerСбоев в окне для открытия
TempData.httpsPort8443 (default)СерверПорт publisher'а, рекламируемый через WifiAwareNetworkSpecifier.setPort
BleServiceData.AWARE_SUPPORTED0x01BLE scan responseБит: пир поддерживает Wi-Fi Aware
BleServiceData.AWARE_RUNNING0x02BLE scan responseБит: Aware-сервис пира сейчас запущен

Сводка проектных компромиссов {#design-trade-offs-recap}

Diagram 14
14

Дополнительная литература

  • Chat Architecture — как WifiAwareTransport вписывается в цепочку fallback'ов LAN → Aware → BLE и более широкий конвейер отправки/получения чата.
  • BLE Transport — транспорт последней инстанции, вступающий при недоступности Aware; также канал, используемый prewarmer'ом для триггера Aware-старта на удалённом пире.
  • Pairing Flow — как устанавливается общий ключ ChaCha20, переиспользуемый как Aware PMK, и как заполняются флаги BLE scan response (AWARE_SUPPORTED / AWARE_RUNNING).