返回部落格
Transport19 min read

Wi-Fi Aware 傳輸層設計 —— 鄰居探索與資料路徑

本文說明 PlainApp 如何使用 Wi-Fi Aware(NAN —— Neighbor Awareness Networking)作為對端傳輸備援鏈的中層,介於 LAN(同子網 HTTPS)與 BLE(最後手段的 GATT RPC)之間。Wi-Fi Aware 讓兩台 PlainApp 裝置在不同 SSID、訪客與 IoT VLAN,或完全沒有 Wi-Fi 基礎設施時仍能通訊——無需從 DHCP 伺服器取得任何 IP 位址。

本文涵蓋僅限 Android 的 Aware 工作階段生命週期、publish / subscribe 探索模型、在框架約 500 ms 視窗內同步雙方 requestNetwork 的兩階段角色切分握手、具閒置清掃的每對端連結集區、讓單一 OkHttp 用戶端同時服務 LAN 與 Aware 的 IPv6 + 自訂 DNS 技巧, 以及透過 BLE 觸發對端 Aware 啟動的預熱器。

關於更廣泛的備援鏈,請參見 Chat Architecture。 關於 Aware 不可用時接手的 BLE 傳輸層,請參見 BLE Transport。關於作為 Aware PMK 重用的共享 ChaCha20 金鑰如何建立,請參見 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 建立一條直接的裝置對裝置資料路徑,完全繞過 基礎設施。
  • 完全沒有基礎設施。 兩台處於荒野、開啟 Wi-Fi 但無 AP 的裝置 仍能聊天。(BLE 也涵蓋此情境,但 Aware 快得多 ——約 10 ms 往返 vs 數秒,以及 MB/s vs 數十 KB/s。)

Diagram 1
1

平台限制

Wi-Fi Aware 在 PlainApp 中僅限 Android:

  • Android 13(API 33)為最低版本——PlainApp 依賴的 WifiAwareNetworkSpecifier.Builder 的 setPort() 與 setPmk() 多載需要 isTPlus()。
  • iOS 未向第三方應用程式公開 Wi-Fi Aware。iOS PlainApp 直接 從 LAN 退到 BLE;WifiAwareTransport 物件甚至未 編譯進 iOS 目標(@RequiresApi(Build.VERSION_CODES.S) + androidMain 原始碼集)。

這就是為什麼 PeerTransportRouter.buildList 呼叫 createWifiAwareTransport() —— 一個在 iOS 上傳回 null 的工廠。

Aware 在備援鏈中的位置 {#where-aware-sits-in-the-fallback-chain}

PlainApp 的 PeerTransportRouter 是一個有序清單。對於每次 send 或 downloadFile 呼叫,它走訪清單並嘗試每個傳輸層直到成功;失敗向下層疊。

Diagram 2
2

為什麼 Aware 是「中間」而非「第一個」?

因為** LAN 在可用時幾乎總是更快**。透過 AP 的同子網 Wi-Fi 跳接是單一次 802.11 框架交換;Aware 資料路徑 增加了 NDP 設定(首次約 5 秒)加上用於裝置對裝置連結的第二個 Wi-Fi 無線電情境。若兩者皆可達,LAN 在延遲 與吞吐量上勝出。

相對地,BLE 永遠較慢——但只要兩台裝置 已配對,它就能運作。Aware 居中:比 BLE 快,比 LAN 慢, 且僅在具備 Wi-Fi 的 Android 13+ 裝置上可用。

工作階段生命週期: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 探索模型是不對稱的:發布者廣告 一個服務,訂閱者掃描它。為讓探索對稱(兩台 裝置都發現彼此),PlainApp 同時做兩者。否則, 裝置 A 必須事先知道對於某個對端它是發布者還是 訂閱者——但對端角色稍後才由 clientId 比較決定(見 探索與角色指派)。

同時 publish 與 subscribe 意味著每台裝置都會看到 對方的 onServiceDiscovered(作為訂閱者)並接收對方的 hello 訊息(作為發布者)——握手的兩個方向 始終可用。

終止時自動重啟

某些 Android 變體(尤其是 MIUI)會終止長時間執行的 Aware 工作階段以節省電力。PlainApp 在 onSessionTerminated 回呼中處理此情況:它將終止的工作階段設為 null,並立即在仍 attached 的 WifiAwareSession 上再次呼叫 publishOwnService / subscribeOwnService。Attach 工作階段本身不會遺失——只有 publish/subscribe 探索工作階段。終止之前的 PeerHandle 會變得過時,這就是為什麼 awaitPeerHandle 會檢查 discoveredAt 時間戳並丟棄超過 30 秒的 PeerHandle。

探索與角色指派 {#discovery--role-assignment}

Wi-Fi Aware 資料路徑協定要求一方擔任 發布者(伺服器),另一方擔任訂閱者(用戶端)。雙方 不能同時擔任發起者——框架會拒絕 沒有匹配對應方的請求。

PlainApp 使用簡單的 clientId 字典序比較,按對端確定性地指派角色:

Diagram 4
4

為什麼是確定性而非協商?

協商方式(例如「較小 MAC 為伺服器」)需要 額外的訊息交換。字典序比較是冪等、 對稱且無狀態的:雙方無需任何通訊即可為 同一對計算出相同角色。clientId 是 13 字元的短 UUID,因此平手(clientId == peer.id)只發生於與 自身比較時——這永遠不會到達傳輸層。

角色決定了下游兩件事:

  1. 誰驅動重試迴圈。 只有用戶端重試 requestNetwork;伺服器對每個收到的 hello 恰好嘗試一次。 這對 500 ms 視窗至關重要(下一節)。
  2. 誰設定連接埠。 發布者呼叫 setPort(httpsPort),因為 它是在自己的 HTTPS 伺服器連接埠上接受入境連線的一方。 訂閱者不設定連接埠——它從 資料路徑建立後的 WifiAwareNetworkInfo 學習對端的連接埠。

兩階段握手(hello + ready) {#the-two-phase-handshake-hello--ready}

Wi-Fi Aware 資料路徑設定最困難的部分是時序。 框架要求雙方在彼此約 500 ms 內呼叫 connectivityManager.requestNetwork ——若一方在另一方註冊其匹配請求之前呼叫,框架會立即以 onUnavailable 拒絕("releaseRequestAsUnfulfillableByAnyFactory")。

PlainApp 透過一個兩訊息的應用層握手解決此問題, 該握手執行於 Aware L2 訊息通道之上(與 onServiceDiscovered 使用的相同 sendMessage API):

Diagram 5
5

為什麼是兩則訊息(hello + ready)而非一則?

單有 hello 不夠,因為方向不對稱。 訂閱者可以在發現發布者的瞬間(在 onServiceDiscovered 中)傳送 hello,但發布者 無法啟動requestNetwork,直到它擁有訂閱者的 PeerHandle,而這只能透過接收 hello 得知。因此 hello 有兩個用途:

  1. 將訂閱者的 PeerHandle 傳遞給發布者。 發布者 需要它來建構 WifiAwareNetworkSpecifier。
  2. 表示連線意圖。 接收 hello 告訴發布者 「訂閱者即將 requestNetwork,所以我也應該這樣做。」

ready 回條是為相反方向而存在——告訴 訂閱者「發布者已註冊其 requestNetwork」。沒有它, 訂閱者的 requestNetwork 可能跑到發布者之前 並被框架拒絕。ready 回條是一個非阻塞 訊號:訂閱者不會在呼叫 requestNetwork 之前等待它(那會增加一次往返),但若它抵達時 訂閱者正處於 IDLE 狀態(重試嘗試之間),訂閱者 可立即重試而無需等待 RETRY_DELAY_MS 間隔。

重試迴圈不對稱

這是設計中最微妙的部分。只有訂閱者 重試。 發布者對每個收到的 hello 恰好嘗試一次 requestNetwork。這是因為:

  • 若雙方獨立重試,它們的重試週期會 失去同步(不同的 delay() 持續時間、不同的 GC 暫停), 兩次 requestNetwork 呼叫鮮少在 500 ms 視窗內重疊。
  • 訂閱者的重試迴圈在每次嘗試時傳送新鮮的 hello, 透過 publishHelloListeners 重新觸發發布者的 buildLink。 這保證發布者的 requestNetwork 永遠在 hello 之後約 50 ms,遠在 500 ms 視窗內。

相關細節記載於 AwarePeerLink.build。

NDP requestNetwork —— 500 ms 視窗 {#ndp-requestnetwork--the-500-ms-window}

requestNetwork 呼叫是 Aware 傳輸層中對時序最敏感的操作。以下是雙方發生的事:

Diagram 6
6

onUnavailable 回呼代表什麼

onUnavailable 在框架找到匹配對端請求之前拒絕 requestNetwork 時觸發。PeerHandle 本身仍然 有效——只有 NDP(Neighbor Discovery Protocol)配對失敗, 因為另一方尚未註冊。PlainApp 在此情況下刻意不呼叫 session.invalidatePeerHandle,因為 使 PeerHandle 失效會丟棄 onServiceDiscovered 曾被呼叫過的唯一訊號(它在每個訂閱工作階段生命週期內對每個對端觸發一次)。保留 PeerHandle 後,重試可 重用它,而不必等待新鮮的探索。

對於從 onMessageReceived 取得的發布者端 PeerHandle 也是如此—— 發布者跨失敗嘗試保留 publishPeerHandles[fromCid] 項目, 因此訂閱者的下一個 hello 會重用快取的 PeerHandle,而非 被丟棄。

每個已配對的對端都擁有自己的 AwarePeerLink 物件,由行程層級的 AwareLinkPool 擁有。集區處理探索事件、連結 重用與閒置驅逐。

Diagram 7
7

為什麼探索時不自動建連結?

集區在 onServiceDiscovered 觸發時明確不建立連結。這是一個關鍵決策:繁忙的咖啡廳 可能有 100 台 PlainApp 裝置同時發布「plain-peer」 服務。若每次探索都觸發 requestNetwork,框架 會被 NDP 設定嘗試淹沒,Wi-Fi 無線電會被 飽和。

相反地,集區只記錄 PeerHandle,並等待以下之一:

  1. 本地使用者傳送訊息 → WifiAwareTransport.send → pool.buildLink(peer)(發送端觸發)。
  2. 遠端對端傳送 hello → onPublishHelloReceived → buildLink(peer)(接收端觸發)。
  3. 遠端對端傳送 ready → onSubscribeReadyReceived → buildLink(peer)(接收端觸發)。

如此一來,只有對於使用者實際正在 交換訊息的對端才會建立連結——而非無線電範圍內的每台 PlainApp 裝置。

閒置清掃

每 10 秒,集區走訪所有連結並關閉任何 lastActiveAt 超過 60 秒者。每次 send 與 downloadFile 都會呼叫 link.touch() 重新整理時間戳。這為使用者已停止 聊天的對端回收 Wi-Fi 無線電情境與 OkHttp 連線集區—— 這很重要,因為 Android 將同時 Aware 資料路徑數量限制為約 4–10 個(因裝置而異)。

IPv6 定址與 plain-aware-peer DNS 技巧 {#ipv6-addressing--the-plain-aware-peer-dns-trick}

Wi-Fi Aware 資料路徑僅使用 link-local IPv6。沒有 IPv4,沒有 DNS 伺服器,沒有 DHCP。對端的 IPv6 位址透過 onCapabilitiesChanged 中的 WifiAwareNetworkInfo.peerIpv6Addr 欄位傳遞——一個只在 Aware 網路介面上有意義的 fe80::... 位址。

PlainApp 需要對此位址傳送 HTTPS 請求,但 OkHttp 的 https:// URL 解析拒絕主機名稱中的原始 IPv6 字面值 (https://[fe80::abcd]:8443/ 可運作,但透過自訂 Dns 解析器路由更為乾淨)。訣竅:

Diagram 8
8

為什麼用哨兵主機名稱?

替代方案——直接在 URL 中傳遞 IPv6 字面值——會 要求每個呼叫端都知道 link-local 位址。透過使用 哨兵主機名稱,URL 建構對 LAN 與 Aware 完全相同: 兩者都產生有效的 https://<host>:<port>/peer_graphql URL,讓 OkHttp 可解析。唯一差異在於繫結至 用戶端的 Dns 實作——LAN 使用系統 DNS,Aware 使用 awareDns(peerIpv6),它對哨兵主機名稱傳回 快取的 link-local 位址,對其他任何主機名稱則 退到 Dns.SYSTEM。

為什麼用 network.socketFactory?

Android 的 Network 物件代表特定的網路介面(在本 例中為 Aware 資料路徑)。透過呼叫 network.socketFactory 並 將它傳遞給 OkHttp 的 socketFactory 設定,我們強制所有 TCP socket 在 Aware 介面上建立——而非預設的 Wi-Fi 或 行動網路介面。若不這麼做,作業系統會透過 預設網路路由請求,而 link-local IPv6 在那裡無法觸及,請求 會以 ENETUNREACH 失敗。

密碼學:PMK 推導與 ChaCha20 重用 {#cryptography-pmk-derivation--chacha20-reuse}

Wi-Fi Aware 支援資料路徑的選用 PMK(Pairwise Master Key)。設定後, L2 連結本身以該 PMK 加密——Wi-Fi 無線電 處理加密,無需應用層密碼學。

PlainApp 從 LanTransport 與 BleTransport 用於應用層加密的相同 ChaCha20 共享金鑰推導出 PMK:

Diagram 9
9

為什麼截斷為 32 位元組?

Wi-Fi Aware PMK 必須恰好 32 位元組(256 位元)。配對得到的 ChaCha20 共享金鑰在正常情況下也是 32 位元組,因此 raw.size == 32 分支是常見路徑。截斷/填補 備援處理金鑰儲存較短的(理論)情況 ——以零填補到 32 位元組是防禦性措施,在 正確配對的對端上實務上不會發生。

已簽章封套與 LAN 相同

由於 createCryptoHttpClient 是 LanTransport 使用的相同工廠, Aware 上的 L7 加密與 LAN 位元對位元相同。 伺服器端的 PeerGraphQLService 不知道(也不在乎)哪個 傳輸層送達請求——它只看到一個已簽章、加密的 GraphQL 負載,並以對端的共享金鑰解密。這就是 Chat Architecture 中記載的 「一份程式碼庫,多種傳輸層」原則。

訊息傳送路徑(端對端) {#message-send-path-end-to-end}

將一切組合起來——當一則聊天訊息透過 Wi-Fi Aware 傳送時發生什麼事:

Diagram 10
10

值得注意的設計選擇

  • 連線重用。 與 BleTransport(在每次請求後拆除 GATT 連線)不同,WifiAwareTransport 重用 Aware 資料路徑 用於使用者在 60 秒閒置 視窗內發出的所有請求。首次請求支付約 400 ms 握手;後續 請求約 10 ms 往返。
  • 與 LAN 相同的密碼學。 ChaCha20 攔截器與已簽章封套與 LAN 位元相同。對端的 PeerGraphQLService 不知道 哪個傳輸層送達請求。
  • 連結失敗時不搶佔。 若 buildLink 失敗,傳輸層 拋出 TransportUnavailable,路由器退到 BLE。 send 內部不重試——AwarePeerLink.build 已執行自己的 內部重試迴圈(用戶端 MAX_BUILD_ATTEMPTS = 1,若 預熱器已預熱雙方則更多)。

檔案下載路徑(端對端) {#file-download-path-end-to-end}

透過 Aware 的檔案下載重用與聊天訊息相同的資料路徑,但 使用獨立的 OkHttp 用戶端,為串流大檔案設定:

Diagram 11
11

為什麼下載用獨立用戶端?

聊天用戶端(AwareHttpClientFactory.build)有 30 秒的 requestTimeoutMillis——適用於 GraphQL 變更,但 對 100 MB 檔案下載是災難性的。下載用戶端 (buildFileDownload)設定:

  • connectTimeoutMillis = 10_000(比聊天的 5 秒長,對 全新資料路徑上慢的首封包更寬容)
  • 每次讀取 readTimeout = 120 s(對比隱含預設的 10 秒)
  • requestTimeoutMillis = 120_000(2 分鐘——對大多數檔案足夠)
  • retryOnConnectionFailure(true)——下載中斷的讀取會 重試,而非讓整個傳輸失敗

它也省略 ChaCha20 攔截器。/fs 端點提供原始 檔案位元組(非已簽章的 GraphQL 封套),而 L2 PMK(存在時) 已加密無線電連結。以 ChaCha20 軟體雙重加密 50 MB 影片 會浪費 CPU 並拖慢傳輸。

串流,而非緩衝

與 BLE 路徑一樣,Aware 下載將檔案透過 ByteReadChannel 串流——檔案在位元組到達時寫入暫存檔, 而非緩衝在記憶體中。PeerFileDownloader 讀取 8 KB 分塊並每秒發出 進度事件。相同的 DownloadedResponse / PeerFileDownloader / DownloadQueue 管線跨所有 傳輸層重用——只有 channel 來源是傳輸層特定的。

預熱:BLE 觸發的 Aware 啟動 {#prewarming-ble-triggered-aware-startup}

Aware 路徑中最大的使用者可見延遲是首次 握手——若雙方都尚未啟動 Aware,使用者的第一則 訊息必須等待:

  1. 本地 Aware 工作階段 attach(約 1 秒)
  2. 本地 publish + subscribe 啟動(約 1 秒)
  3. 遠端對端的 Aware 啟動(透過 BLE 約 2 秒)
  4. 相互探索(約 1 秒)
  5. NDP 握手(約 400 ms)

這是在第一個位元組傳送前約 5 秒。為隱藏此延遲, PeerTransportPrewarmer 在進入 ChatPage 時執行,並 透過 BLE 觸發遠端對端的 Aware 啟動:

Diagram 12
12

BLE 的雙重角色

BLE 在此有兩個用途:

  1. 讀取對端當前的 Aware 狀態(便宜,無需 GATT 連線—— 掃描回應的 serviceData byte0 帶有 Aware 旗標)。
  2. 觸發對端啟動 Aware(若它支援但目前 未運行)。這透過常規 BleTransport.send 路徑進行—— 一個以共享 ChaCha20 金鑰加密的 startAware GraphQL 變更, 透過 GATT RPC 送達對端的 /peer_graphql 端點。

這是少數幾處傳輸層是合作而非單純備援的地方:BLE 用於 預先搶佔升級工作階段到更快的 Aware 傳輸層, 在使用者察覺之前。

為什麼樂觀地 setAwareRunning(true)?

startAware 變更在遠端對端的解析器呼叫 WifiAwareTransport.start() 後立即傳回成功——但 Aware 工作階段 尚未實際 attached(onAttached 非同步觸發)。PlainApp 樂觀地將對端標記為 awareRunning = true,因為:

  • 若它確實啟動了,下一次 send 會使用 Aware(快)。
  • 若沒有(例如對端的 Wi-Fi 關閉),下一次 send 的 buildLink 會以 TransportUnavailable 失敗並自然退到 BLE。
  • 偽陽性的代價是一次約 5 秒的逾時,而非永久 阻塞——PeerCircuitBreaker 記錄失敗但不開啟 BLE 那一腿(BLE 只在自身的失敗時開啟)。

為什麼節流為 30 秒?

PeerTransportPrewarmer.prewarm(peerId) 對每個對端記錄一個時間戳, 並拒絕在 30 秒內重新執行。這是因為使用者頻繁地在 聊天清單與聊天頁面之間來回導航——若不 節流,每次導航都會觸發 BLE 掃描 + startAware 變更,耗電並對 BLE 無線電發出垃圾請求。30 秒視窗 短到足以捕捉剛上線的對端(例如使用者在 遠端裝置上開啟應用程式),但長到足以避免虛假的重跑。

失敗模式與快速跳過旗標 {#failure-modes--the-fast-skip-flag}

Aware 比任何其他傳輸層有更多失敗模式。 isAwareRunning 快速跳過旗標是整個模組中最重要的單一 最佳化——若沒有它,每次 send 都會 浪費 10 秒在 buildLink 逾時後才退回 BLE。

Diagram 13
13

isAwareRunning 旗標是關鍵

若沒有這個單一布林值,每次 Aware send 都會:

  • 永遠嘗試 buildLink → 對 Aware 未運行的對端每次 send 都 10 秒逾時。
  • 永遠跳過 Aware → 即使雙方都運行也永遠不用它。

該旗標從兩個來源重新整理,依權威排序:

  1. BLE 掃描回應(便宜,無需 GATT 連線)——由 PeerTransportPrewarmer.refreshAwareFlagFromScan 設定。對端在 9 位元組的 serviceData 負載(byte0 位元欄位)中廣告 其 Aware 狀態。
  2. GATT DISCOVER 回覆(權威)——由 PairingTransport.scanAndDiscover 在完整探索發生時設定。這會 覆寫掃描提示。

為 false 時,WifiAwareTransport.send 與 downloadFile 拋出 TransportUnavailable 立即——無掃描、無握手、無 逾時。路由器在微秒內退到 BLE。

關鍵常數參考 {#key-constants-reference}

常數值所在位置用途
AwareSession.SERVICE_NAME"plain-peer"探索每台 PlainApp 裝置 publish 與 subscribe 的服務名稱
AwareSession.PEER_HANDLE_MAX_AGE_MS30 000PeerHandle 快取捨棄過時 PeerHandle(對端的 publish 工作階段可能已重啟)
AwareSession.READY_TIMEOUT_MS15 000握手訂閱者等待發布者 ready 回條
AwareSession.MSG_HELLO0握手訂閱者 → 發布者 訊息 ID
AwareSession.MSG_READY1握手發布者 → 訂閱者 訊息 ID
AwarePeerLink.MAX_BUILD_ATTEMPTS1握手(僅用戶端)單次嘗試——原本 3,因預熱器預熱雙方現改為 1
AwarePeerLink.ATTEMPT_TIMEOUT_MS5 000握手每次嘗試逾時——原本 10 秒,減半以加速備援
AwarePeerLink.RETRY_DELAY_MS500握手重試之間的延遲(僅用戶端)
AwarePeerLink.REQUEST_TIMEOUT_MS30 000NDPconnectivityManager.requestNetwork 逾時
AwareLinkPool.IDLE_TIMEOUT_MS60 000集區清掃60 秒不活動後關閉閒置連結
AwareLinkPool.IDLE_SWEEP_INTERVAL_MS10 000集區清掃清掃間隔
AwareHttpClientFactory.AWARE_HOST"plain-aware-peer"DNS由自訂 Dns 解析為對端 IPv6 的哨兵主機名稱
build 聊天用戶端connectTimeout 5 秒, requestTimeout 30 秒, ChaCha20 攔截器
buildFileDownloadconnectTimeout 10 秒, readTimeout 120 秒, requestTimeout 120 秒, 無加密
PeerTransportPrewarmer.PREWARM_TTL_MS30 000預熱每對端節流
PeerTransportPrewarmer.BLE_SCAN_TIMEOUT_MS15 000預熱refreshAwareFlagFromScan 的 BLE 掃描逾時
PeerCircuitBreaker.WINDOW_MS30 000斷路器達到閾值後的開啟持續時間
PeerCircuitBreaker.MAX_FAILURES2斷路器視窗內開啟所需的失敗次數
TempData.httpsPort8443 (預設)伺服器透過 WifiAwareNetworkSpecifier.setPort 廣告的發布者連接埠
BleServiceData.AWARE_SUPPORTED0x01BLE 掃描回應表示對端支援 Wi-Fi Aware 的位元
BleServiceData.AWARE_RUNNING0x02BLE 掃描回應表示對端的 Aware 服務目前正在運行的位元

設計取捨回顧 {#design-trade-offs-recap}

Diagram 14
14

延伸閱讀

  • Chat Architecture —— WifiAwareTransport 如何融入 LAN → Aware → BLE 備援鏈與更宏觀的聊天 傳送/接收管線。
  • BLE Transport —— Aware 不可用時 接手的最後手段傳輸層;也是預熱器用來觸發 遠端對端 Aware 啟動的通道。
  • Pairing Flow —— 作為 Aware PMK 重用的共享 ChaCha20 金鑰如何建立, 以及 BLE 掃描回應旗標 (AWARE_SUPPORTED / AWARE_RUNNING)如何填入。