ब्लॉग पर वापस जाएँ
Transport15 min read

BLE ट्रांसपोर्ट डिज़ाइन — संदेश और फ़ाइल डाउनलोड

यह लेख बताता है कि PlainApp चैट संदेशों को कैसे पुश करता है और फ़ाइलों को Bluetooth Low Energy पर कैसे डाउनलोड करता है जब न तो LAN और न ही Wi-Fi Aware उपलब्ध हो। BLE गारंटीड फ़ॉलबैक है: धीमा, लेकिन बिना किसी IP कनेक्टिविटी के काम करता है। यह लेख वायर फ़ॉर्मैट, दो-परत चंकिंग डिज़ाइन, समवर्ती ट्रैफ़िक को प्राथमिकता कैसे दी जाती है (और कैसे नहीं), और हर अनुरोध के बाद हर कनेक्शन क्यों तोड़ दिया जाता है, कवर करता है।

इस ट्रांसपोर्ट का उपयोग करने वाली व्यापक चैट आर्किटेक्चर के लिए देखें Chat Architecture। दो डिवाइस कैसे हर BLE पेलोड को कूटलेखित करने वाली साझा ChaCha20 कुंजी प्राप्त करते हैं, उसके लिए देखें Pairing Flow

विषय-सूची

BLE ट्रांसपोर्ट आख़िर क्यों?

PlainApp सर्वररहित और ऑफ़लाइन-फ़र्स्ट है। ट्रांसपोर्ट परत एक क्रमित फ़ॉलबैक श्रृंखला है: LAN → Wi-Fi Aware → BLE। LAN सुखद पथ है (Wi-Fi पर HTTPS, ~10 ms राउंड ट्रिप)। Wi-Fi Aware क्रॉस-सबनेट पीयर कवर करता है (अलग SSID, गेस्ट बनाम IoT VLAN)। दोनों को किसी प्रकार की IP कनेक्टिविटी चाहिए। BLE एकमात्र ट्रांसपोर्ट है जो तब काम करता है जब:

  • डिवाइस बिल्कुल एक ही IP नेटवर्क पर न हों।
  • Wi-Fi बंद हो या एयरप्लेन मोड में हो (BLE रेडियो अलग होता है)।
  • Wi-Fi Aware असमर्थित हो (Android < 13, PlainApp के सभी iOS वेरिएंट)।

BLE धीमा है — दसियों KB/s, प्रति अनुरोध सेकंड की लेटेंसी — लेकिन किसी भी पेयर्ड पीयर के लिए गारंटीड है, क्योंकि इसे केवल पीयर के clientId की ज़रूरत है, जो हमेशा BLE स्कैन प्रतिक्रिया में प्रसारित होता है।

Diagram 1
1

GATT सेवा लेआउट

PlainApp दो characteristics वाली एक कस्टम GATT सेवा विज्ञापित करता है। कोई पंजीकृत 16-बिट UUID नहीं है — सेवा 128-बिट UUID का उपयोग करती है जिसके अंतिम बाइट्स ASCII-डिकोड होकर plpai\x01 बनते हैं:

Diagram 2
2

दो characteristics क्यों?

दो प्रोटोकॉल के पूरी तरह से अलग ट्रस्ट मॉडल और पेलोड आकार हैं:

  • NEARBY पेयरिंग संदेश ले जाता है। वे पीयर के पेयर्ड होने से पहले आते हैं (अभी कोई साझा कुंजी नहीं), इसलिए वे अपने खुद के Ed25519-हस्ताक्षरित JSON पेलोड अपने खुद के उपसर्ग राउटिंग के साथ उपयोग करते हैं। बॉडी एक सादा स्ट्रिंग है।
  • HTTP पेयरिंग के बाद का सारा ट्रैफ़िक (चैट, फ़ाइलें, प्रेज़ेंस) ले जाता है। यह हमेशा साझा कुंजी से ChaCha20-कूटलेखित होता है और LAN Ktor सर्वर की तरह ही HttpRouteRegistry उपयोग करता है, इसलिए रूट हैंडलर्स (/peer_graphql, /fs, /peer_status) एक बार लिखे जाते हैं और दोनों ट्रांसपोर्ट के लिए पुनः उपयोग होते हैं।

रीड के बजाय नोटिफ़िकेशन क्यों?

BLE ATT प्रोटोकॉल एक एकल एट्रिब्यूट रीड को 512 बाइट तक सीमित करता है। एक GraphQL प्रतिक्रिया या 16 KB फ़ाइल चंक कहीं बड़ा हो सकता है। PlainApp इसे वास्तविक डेटा के लिए readCharacteristic कभी उपयोग न करके घूमता है — सर्वर का onCharacteristicReadRequest GATT_SUCCESS के साथ खाली पेलोड लौटाता है। इसके बजाय, क्लाइंट अपना अनुरोध characteristic में लिखता है, और सर्वर चंक्ड नोटिफ़िकेशन के अनुक्रम भेजकर प्रतिक्रिया देता है जिसे क्लाइंट पुनः जोड़ता है। यह BleDeviceApi.requestAsync, BleServerProtocol.handleWrite, और AndroidBleGattServer.sendChunkedResponse में प्रलेखित है।

पीयर पहचान: shortId, MAC नहीं

BLE विज्ञापन पैकेट छोटे होते हैं (31 बाइट) और BLE MAC पता Android द्वारा हर ~15 मिनट में यादृच्छिक बनाया जाता है — इसलिए इसे स्थिर पहचानकर्ता के रूप में उपयोग नहीं किया जा सकता। PlainApp इसके बजाय स्कैन प्रतिक्रिया में 9-बाइट serviceData पेलोड प्रसारित करता है:

Diagram 3
3

पूर्ण clientId के बजाय ट्रंकेटेड हैश क्यों?

एक 13-अक्षर clientId 13 बाइट में फिट हो जाएगा, लेकिन PlainApp दो कारणों से 8-बाइट ट्रंकेटेड SHA-256 चुनता है:

  1. स्थिर बाइट बजट। कुल 9 बाइट 31-बाइट विज्ञापन पेलोड में सेवा UUID (16 बाइट), लंबाई, और टाइप फ़ील्ड्स के साथ आराम से फिट हो जाते हैं (~27 बाइट उपयोग, 4 बाइट हेडरूम)।
  2. गोपनीयता। BLE स्कैन करने वाला निष्क्रिय पर्यवेक्षक shortId से clientId को पुनः प्राप्त नहीं कर सकता (SHA-256 हैश का 8-बाइट उपसर्ग व्यवहार में अनुत्क्रमणीय है)। वे केवल उस पीयर को पहचान सकते हैं जिसे उन्होंने पहले उसी shortId का विज्ञापन करते देखा था — वे PlainApp उपयोगकर्ताओं की गणना नहीं कर सकते।

पूर्ण clientId केवल उस पीयर को प्रकट होता है जो वास्तव में GATT पर कनेक्ट हुआ है और DDiscoverReply का आदान-प्रदान किया है — यानी वह पीयर जिसके साथ उपयोगकर्ता ने पहले ही बातचीत करना चुना है।

दो-परत चंकिंग डिज़ाइन

यह BLE ट्रांसपोर्ट का सबसे सूक्ष्म हिस्सा है, और दोनों परतों को समझना आवश्यक है क्योंकि उनके आकार और उद्देश्य पूरी तरह से अलग हैं:

Diagram 4
4

380 अक्षर क्यों?

निगोशिएटेड ATT MTU Android पर 517 बाइट है (requestMtu(517) — BLE विनिर्देश द्वारा अधिकतम अनुमत) और iOS पर ~185+ (CoreBluetooth द्वारा स्वतः-निगोशिएटेड)। ATT हेडर (~3 बाइट) और BleSegmentData के JSON रैपर ओवरहेड ({"d":"...","s":N} ~12 बाइट जोड़ता है) घटाने पर, 380 अक्षर पेलोड दोनों प्लेटफ़ॉर्म पर एक एकल ATT MTU में आराम से फिट होते हैं। मान सममित है (क्लाइंट अनुरोध फ़्रैग्मेंट और सर्वर नोटिफ़िकेशन फ़्रैग्मेंट दोनों 380 उपयोग करते हैं), जो कोड को सरल रखता है।

फ़ाइल चंक के लिए 16 KiB क्यों?

एक 16 KiB फ़ाइल चंक base64-एन्कोड होकर ~22 KiB JSON बनता है, जो ~58 GATT नोटिफ़िकेशन सेगमेंट में विभाजित होता है। BLE पर हर requestAsync राउंड-ट्रिप सेकंड लेता है, इसलिए कम-लेकिन-बड़े चंक प्रति-चंक ओवरहेड कम करते हैं। बहुत बड़ा जाना BLE RPC टाइमआउट हिट करने का जोखिम उठाता और खराब प्रोग्रेस फ़ीडबैक देता (उपयोगकर्ता को प्रोग्रेस केवल प्रति चंक एक बार अपडेट दिखता)। 16 KiB अनुभवजन्य रूप से ट्यून किया गया अनुकूल बिंदु है — थ्रूपुट के लिए पर्याप्त बड़ा, उत्तरदायी प्रोग्रेस UI के लिए पर्याप्त छोटा।

RPC प्रिमिटिव: BleDeviceApi.requestAsync {#the-rpc-primitive-bledeviceapirequestasync}

हर BLE चैट संदेश और हर फ़ाइल चंक BleDeviceApi.requestAsync(service, requestData) का एक कॉल है — एक suspend फंक्शन जो BleResult लौटाता है। यह कॉलर के दृष्टिकोण से सिंक्रोनस है: एक अनुरोध → एक पूरी तरह पुनः-जोड़ी गई प्रतिक्रिया, कोई पाइपलाइनिंग नहीं।

Diagram 5
5

मुख्य अपरिवर्तनीयताएँ

  1. एक अनुरोध → एक प्रतिक्रिया। requestAsync कॉलर के दृष्टिकोण से सिंक्रोनस है — यह केवल पूरी प्रतिक्रिया पुनः-जुड़ने के बाद लौटता है। कोई पाइपलाइनिंग नहीं।
  2. प्रति-कॉल नोटिफ़िकेशन सक्षम। क्लाइंट हर requestAsync की शुरुआत में CCCD लिखता है और अंत में इसे अक्षम करता है। यह बेकार है (प्रति कॉल दो अतिरिक्त GATT राइट) लेकिन प्रोटोकॉल को स्टेटलेस रखता है — सर्वर को यह ट्रैक नहीं करना पड़ता कि कौन-से क्लाइंट "सुन" रहे हैं।
  3. RPC के भीतर कोई पुनः प्रयास नहीं। यदि कोई एक writeCharacteristic टाइमआउट (5 सेकंड) हो जाता है, तो पूरा RPC निरस्त हो जाता है। केवल ensureConnected पुनः प्रयास करता है (कनेक्ट विफलता पर 3 प्रयास)। PeerCircuitBreaker द्वारा मोटा ट्रांसपोर्ट-स्तरीय बैकऑफ़ प्रदान किया जाता है, RPC परत द्वारा नहीं।

वायर एनवलप फ़ॉर्मैट {#wire-envelope-format}

Layer A सेगमेंट के अंदर का पेलोड एक नेस्टेड JSON एनवलप है। विभाजन हटाने पर, लॉजिकल संरचना है:

Diagram 6
6

प्रतिक्रिया आकार

प्रतिक्रिया उसी Layer A विभाजन से विपरीत दिशा में बहती है, लेकिन भीतरी JSON तीन फ़ील्ड वाला BleHttpResponse है: s (HTTP स्टेटस कोड), h (प्रतिक्रिया हेडर मैप), और b (बॉडी)। बॉडी BleHttpCall.encodeResponse() द्वारा हमेशा base64-एन्कोडेड होती है, तब भी जब खाली हो — प्रतिक्रिया बाइनरी हो सकती है (कूटलेखित GraphQL बाइट्स, कच्चे /fs फ़ाइल बाइट्स) और BLE ट्रांसपोर्ट केवल स्ट्रिंग है, इसलिए वही JSON एनवलप टेक्स्ट और बाइनरी दोनों पेलोड ले जाता है।

संदेश भेजने का पथ (अंत-से-अंत)

सब कुछ एक साथ जोड़ने पर — जब एक चैट संदेश BLE पर भेजा जाता है तो क्या होता है:

Diagram 7
7

उल्लेखनीय डिज़ाइन चुनाव

  • LAN के समान कुंजी। पेयरिंग से ChaCha20 साझा कुंजी BLE के लिए पुनः उपयोग होती है — कोई अलग BLE कुंजी नहीं। LanTransport द्वारा उपयोग किया जाने वाला OkHttp क्रिप्टो इंटरसेप्टर और BleTransport में मैन्युअल chaCha20Encrypt/chaCha20Decrypt एक ही प्रिमिटिव हैं, बस अलग तरीके से लागू।
  • LAN के समान रूट हैंडलर्स। BleHttpRequest HttpRouteRegistry.matchRoute(path) के माध्यम से डिस्पैच होता है, जो वही रजिस्ट्री है जो Ktor LAN सर्वर उपयोग करता है। इसलिए /peer_graphql, /fs, /peer_status आदि एक बार कार्यान्वित होते हैं और दोनों ट्रांसपोर्ट पर समान रूप से काम करते हैं।
  • कोई कनेक्शन पुनः उपयोग नहीं। finally { scanner.teardownConnection(client) } ब्लॉक हमेशा चलता है। हर संदेश पूरा connect→discoverServices→MTU लागत (~सेकंड) चुकाता है। यह एक जानबूझकर ट्रेड-ऑफ़ है — देखें Design Trade-offs

फ़ाइल डाउनलोड पथ (अंत-से-अंत)

BLE पर डाउनलोड स्ट्रीमिंग होते हैं — फ़ाइल 16 KiB चंक में पढ़ी जाती है और आते ही एक टेम्प फ़ाइल में लिखी जाती है, इसलिए 10 MB फ़ाइल को 10 MB RAM की ज़रूरत नहीं। बात यह है कि हर चंक का RPC एक अलग requestAsync कॉल है, और चंक्स को एक ByteChannel में धकेला जाता है जिसे उपभोक्ता समवर्ती रूप से पढ़ता है।

Diagram 8
8

स्ट्रीमिंग क्यों, एक बड़े RPC के बजाय?

एक 10 MB फ़ाइल एकल RPC के रूप में भेजने का अर्थ ~280 000 नोटिफ़िकेशन सेगमेंट, सभी दोनों पक्षों पर मेमोरी में तब तक रखे जाएँगे जब तक प्रतिक्रिया शुरू भी न हो — और पूरा स्थानांतरण तभी सफल होना चाहिए जब कोई प्रोग्रेस रिपोर्ट हो। और भी बुरा, बीच में एक ड्रॉप हुआ नोटिफ़िकेशन पूरी चीज़ को भ्रष्ट कर देता।

चंक्ड डिज़ाइन के तीन फायदे हैं:

  1. स्थिर मेमोरी। एक समय में केवल एक 16 KiB चंक इन-फ़्लाइट होता है।
  2. लाइव प्रोग्रेस। DownloadQueue.notifyProgressUpdate() हर सेकंड फायर करता है, और UI डाउनलोड बार दिखाता है।
  3. लचीलापन। एक विफल चंक स्वतंत्र रूप से पुनः प्रयास हो सकता है (DownloadQueue कार्य स्तर पर pause/resume/retry का समर्थन करता है; एक मिड-स्ट्रीम विफलता आंशिक टेम्प फ़ाइल छोड़ देती है, हालाँकि वर्तमान में डाउनलोडर इसे विफलता पर मिटा देता है — ट्रेड-ऑफ़ देखें)।

onClose डाउनलोड जॉब को क्यों रद्द करता है

DownloadedResponse.onClose कॉलबैक downloadJob.cancel() को कॉल करता है। यह आवश्यक है क्योंकि डाउनलोड लूप एक चाइल्ड कोरूटीन में चलता है जो अन्यथा हमेशा चलता रहेगा यदि उपभोक्ता चैनल को जल्दी छोड़ दे (जैसे उपयोगकर्ता ने Pause टैप किया)। DownloadedResponse पर AutoCloseable अनुबंध का अर्थ है कि उपभोक्ता का use { ... } ब्लॉक बाहर निकलने पर स्वतः onClose को लागू करता है, BLE डाउनलोड कोरूटीन को रद्द करता है और कोरूटीन के finally ब्लॉक में GATT कनेक्शन तोड़ता है।

प्राथमिकता: चैट व्यवहार में फ़ाइलों को कैसे हराती है {#prioritization-how-chat-beats-files-in-practice}

यह किसी भी चैट एप्लिकेशन के लिए सबसे महत्वपूर्ण सवाल है: जब एक धीमा BLE फ़ाइल डाउनलोड चल रहा हो, तो क्या एक नया चैट संदेश उससे आगे कूद सकता है?

ईमानदार जवाब: कोई स्पष्ट प्राथमिकता योजना नहीं है

BLE कोड या डाउनलोड कतार में कोई प्राथमिकता फ़ील्ड, कोई प्राथमिकता कतार, कोई प्रीएम्पशन नहीं है। मैंने इसे संपूर्ण grep द्वारा सत्यापित किया — shared/src में एकमात्र priority मिलान लॉग-प्राथमिकता स्तर और EXIF मेटाडेटा हैं, संदेश-बनाम-डाउनलोड अनुक्रमण से कोई संबंध नहीं।

इसके बजाय जो मौजूद है वह आर्किटेक्चरल विभाजनों का एक समूह है जो वांछित व्यवहार को एक उभरते गुण के रूप में तैयार करते हैं:

Diagram 9
9

यह व्यवहार में क्यों काम करता है

वह विभाजन जो चैट को "प्राथमिकता महसूस" होने देता है, संरचनात्मक है:

  1. चैट भेजने DownloadQueue से नहीं गुज़रते। वे सीधे PeerGraphQLClientPeerTransportRouterBleTransport.send द्वारा जारी होते हैं। इसलिए एक चैट संदेश कभी फ़ाइल डाउनलोड की कतार के पीछे नहीं बैठता।
  2. हर BleTransport कॉल अपना GATT कनेक्शन खोलता है। एक लंबा डाउनलोड एक कनेक्शन पकड़े होने से चैट भेजने को उसी पीयर के लिए दूसरा कनेक्शन खोलने से नहीं रोकता। Android कई समवर्ती GATT कनेक्शन का समर्थन करता है।
  3. चैट RPC छोटे होते हैं। एक एकल चैट संदेश एक requestAsync राउंड ट्रिप है (कनेक्ट के बाद ~1 सेकंड)। भले ही रेडियो डाउनलोड से व्यस्त हो, चैट भेजना कुछ सेकंड में पूरा हो जाता है।

जहाँ डिज़ाइन कमी रखता है

"कोई स्पष्ट प्राथमिकता नहीं" के ट्रेड-ऑफ़:

  • कनेक्ट लेटेंसी। दोनों चैट और डाउनलोड हर बार connect→discover→MTU लागत (~सेकंड) चुकाते हैं, क्योंकि कनेक्शन पुनः उपयोग नहीं होते। डाउनलोड के दौरान आने वाला चैट संदेश डाउनलोड के मौजूदा कनेक्शन पर सवार नहीं हो सकता — वह नया खोलता है।
  • Android पर स्टेटिक कतार। AndroidBleGattClient में प्रोसेस-व्यापी operationQueue सभी पीयर और सभी कनेक्शन में GATT संक्रियाओं को क्रमबद्ध करता है। इसलिए जबकि दो GATT कनेक्शन सह-अस्तित्व रख सकते हैं, उनकी write/read/notify संक्रियाएँ कतार स्तर पर अंतर्विरामित होती हैं। व्यवहार में यह ठीक है (हर संक्रिया ~ms है) लेकिन यह उच्च समवर्तितता के तहत एक सूक्ष्म वैश्विक बाधा है।
  • कोई प्रीएम्पशन नहीं। चल रहे डाउनलोड को चैट संदेश को जाने देने के लिए रोका नहीं जा सकता। चैट भेजना बस समवर्ती रूप से चलता है और रेडियो समय के लिए प्रतिस्पर्धा करता है।

एक भविष्य सुधार BleTransport.send और downloadFile के चारों ओर प्रति-पीयर Mutex और कतार पर एक प्राथमिकता फ़ील्ड हो सकता है — लेकिन वर्तमान डिज़ाइन इस तथ्य पर निर्भर करता है कि चैट RPC पर्याप्त छोटे हैं कि प्रतिस्पर्धा शायद ही उपयोगकर्ता-दृश्य हो।

समवर्ती नियंत्रण और स्टेटिक GATT कतार

यह अपने अनुभाग का हकदार है क्योंकि यह Android BLE कार्यान्वयन का सबसे सूक्ष्म पहलू है।

Diagram 10
10

स्टेटिक (प्रोसेस-व्यापी) क्यों?

Android BLE स्टैक एक एकल BluetoothGatt इंस्टेंस पर समवर्ती GATT संक्रियाओं की अनुमति नहीं देता — किसी और राइट इन-फ़्लाइट होने पर writeCharacteristic कॉल करने पर false लौटता है और दूसरी राइट चुपचाप ड्रॉप हो जाती है। मानक समाधान प्रति-BluetoothGatt कतार है। PlainApp एक कदम आगे जाता है और एक प्रोसेस-व्यापी कतार (companion object में) उपयोग करता है, जो अति-रूढ़िवादी लेकिन सही है: यह गारंटी देता है कि ऐप में कहीं भी कोई दो GATT संक्रियाएँ एक साथ नहीं चलतीं।

इसकी कीमत यह है कि एक लंबे BLE फ़ाइल डाउनलोड की write/read/notify संक्रियाएँ किसी और पीयर की GATT संक्रियाओं के पीछे कतारबद्ध होती हैं (और पीछे से कतारबद्ध करती हैं)। चूँकि हर व्यक्तिगत संक्रिया ~ms है, यह शायद ही कभी उपयोगकर्ता-दृश्य बाधा है — लेकिन कई पीयर के लिए भारी समवर्ती BLE ट्रैफ़िक के तहत, यह बन सकता है।

ट्रांसपोर्ट परत पर कोई प्रति-पीयर लॉक नहीं

BleDeviceApi.requestAsync एक सादा suspend fun है जिसमें कोई mutex, कोई कतार, कोई प्रति-पीयर क्रमानुसार नहीं है। एक ही पीयर के लिए BleTransport.send के दो समवर्ती कॉल अपना GATT कनेक्शन खोलेंगे और स्वतंत्र रूप से आगे बढ़ेंगे। क्रमानुसार अंतर्निहित रूप से GATT संक्रिया स्तर पर होता है (Android पर स्टेटिक कतार के माध्यम से, या iOS पर अनुक्रमिक await के माध्यम से)।

कनेक्शन जीवनचक्र और MTU निगोशिएशन {#connection-lifecycle--mtu-negotiation}

Diagram 11
11

requestMtu(517) क्यों?

डिफ़ॉल्ट ATT MTU 23 बाइट है (3-बाइट ATT हेडर के बाद केवल 20 बाइट पेलोड)। डिफ़ॉल्ट MTU के साथ, हर 380-अक्षर सेगमेंट को 1 के बजाय ~19 GATT राइट की ज़रूरत होती — 19× की मंदी। BLE विनिर्देश द्वारा अनुमत अधिकतम MTU (517 बाइट) का अनुरोध करने से 380-अक्षर सेगमेंट एक एकल ATT संक्रिया में फिट हो जाते हैं, थ्रूपुट में नाटकीय सुधार।

iOS कोई स्पष्ट MTU अनुरोध API उजागर नहीं करता — CoreBluetooth इसे कनेक्शन के दौरान परिधि के साथ स्वतः निगोशिएट करता है। आधुनिक iOS डिवाइस आमतौर पर ~185 बाइट निगोशिएट करते हैं, जो अभी भी 380-अक्षर सेगमेंट के लिए पर्याप्त है (ATT हेडर + JSON रैपर ओवरहेड घटाने के बाद)।

नोटिफ़िकेशन के लिए फ्लो कंट्रोल {#flow-control-for-notifications}

सर्वर प्रतिक्रिया फ़्रैगमेंट नोटिफ़िकेशन के रूप में भेजता है, लेकिन BLE नोटिफ़िकेशन में कोई अंतर्निहित फ्लो कंट्रोल नहीं है — यदि सर्वर नियंत्रक के प्रेषण की तुलना में तेज़ नोटिफ़िकेशन भेजता है, तो वे चुपचाप ड्रॉप हो जाते हैं। PlainApp स्पष्ट ack-आधारित फ्लो कंट्रोल कार्यान्वित करता है:

Diagram 12
12

इस फ्लो कंट्रोल के बिना, बैक-टू-बैक नोटिफ़िकेशन BLE नियंत्रक द्वारा चुपचाप ड्रॉप किए जाएँगे जब इसकी आंतरिक भेजने की कतार भर जाए — एक ज्ञात Android BLE समस्या जो BleGattServer इंटरफ़ेस टिप्पणियों में प्रलेखित है। प्रति-डिवाइस एकल-इन-फ़्लाइट नियम गारंटी देता है कि हर नोटिफ़िकेशन या तो प्रेषित होता है या टाइमआउट ट्रिगर करता है (जिसे तब एक ट्रांसपोर्ट विफलता के रूप में माना जाता है)।

त्रुटि प्रबंधन: TransportUnavailable बनाम वास्तविक विफलता {#error-handling-transportunavailable-vs-real-failure}

TransportUnavailable वह संकेत है जो PeerTransportRouter को अगले ट्रांसपोर्ट पर गिरने को कहता है। और कुछ भी कॉलर को लौटाई गई वास्तविक विफलता है।

Diagram 13
13

डाउनलोड विफलता सूक्ष्मता

BleTransport.downloadFile DownloadedResponse(200, channel, onClose) तुरंत लौटाता है — चंक्ड डाउनलोड लूप एक पृष्ठभूमि कोरूटीन में चलता है जो चैनल में लिखता है। यदि कोई चंक RPC मिड-स्ट्रीम विफल होता है, तो लूप channel.close(TransportUnavailable(...)) को कॉल करता है, जिसका अर्थ है कि उपभोक्ता (PeerFileDownloader.downloadAsync) को त्रुटि channel.readAvailable(buf) से फेंकी गई एक्सेप्शन के रूप में दिखती है।

इसका अर्थ है कि PeerTransportRouter.downloadFile कॉल स्वयं सफल रहा (DownloadedResponse लौटाया), इसलिए सर्किट ब्रेकर मिड-स्ट्रीम डाउनलोड त्रुटियों के लिए विफलता दर्ज नहीं करता। केवल कनेक्ट-समय और स्कैन-समय विफलताएँ राउटर द्वारा पकड़ी जाती हैं। यह एक जानबूझकर डिज़ाइन चुनाव है — एक मिड-स्ट्रीम विफलता को उस पीयर के लिए BLE को स्थायी रूप से अक्षम नहीं करना चाहिए (शायद पीयर अस्थायी रूप से सीमा से बाहर चला गया हो)।

मुख्य स्थिरांक संदर्भ {#key-constants-reference}

ConstantValueWherePurpose
BleDeviceApi.CHUNK_SIZE380GATT सेगमेंट विभाजनहर BleSegmentData.data का आकार (JSON ओवरहेड के बाद ATT MTU में फिट)
BleTransport.CHUNK_SIZE16 384 (16 KiB)फ़ाइल-डाउनलोड बाइट-रेंजहर /fs चंक अनुरोध का आकार
BleTransport.SCAN_TIMEOUT_MS10 000BLE स्कैनscanner.findOne के लिए टाइमआउट
BleDeviceApi.NOTIFY_TIMEOUT_MS15 000RPC प्रतिक्रियाrequestAsync में प्रति-नोटिफ़िकेशन प्रतीक्षा
AndroidBleGattClient MTU517कनेक्शन सेटअपrequestMtu(517) — BLE विनिर्देश द्वारा अधिकतम अनुमत
AndroidBleGattClient connect timeout10 000कनेक्शन सेटअपSTATE_CONNECTED के लिए प्रतीक्षा
AndroidBleGattClient MTU timeout5 000कनेक्शन सेटअपonMtuChanged के लिए प्रतीक्षा
AndroidBleGattClient write timeout5 000GATT राइटonCharacteristicWrite के लिए प्रतीक्षा
AndroidBleGattClient read timeout10 000GATT रीडonCharacteristicRead के लिए प्रतीक्षा (वास्तविक डेटा के लिए अप्रयुक्त)
AndroidBleGattClient notify-state timeout5 000CCCD राइटCCCD डिस्क्रिप्टर राइट के लिए प्रतीक्षा
ensureConnected retries3कनेक्शन सेटअपकुल 4 प्रयास तक (0..3)
AndroidBleGattServer.NOTIFY_ACK_TIMEOUT_MS10 000नोटिफ़िकेशन फ्लो कंट्रोलonNotificationSent के लिए प्रतीक्षा
AndroidBleGattServer notifyChunkSize380प्रतिक्रिया विभाजनBleDeviceApi.CHUNK_SIZE के समान
IosBleGattServer retry cap10नोटिफ़िकेशन फ्लो कंट्रोलहार मानने से पहले अधिकतम updateValue पुनः प्रयास
PeerCircuitBreaker.WINDOW_MS30 000ट्रांसपोर्ट सर्किट ब्रेकरथ्रेशोल्ड के बाद खुली अवधि
PeerCircuitBreaker.MAX_FAILURES2ट्रांसपोर्ट सर्किट ब्रेकरखोलने के लिए विंडो के भीतर विफलताएँ
DownloadQueue.MAX_CONCURRENT3डाउनलोड वर्कर पूलसमवर्ती डाउनलोड कोरूटीन
BleServiceData.SHORT_ID_BYTES8पीयर पहचानट्रंकेटेड SHA256 उपसर्ग बाइट
BleServiceData.PAYLOAD_BYTES9पीयर पहचान1 फ़्लैग्स बाइट + 8 shortId बाइट
BleSegmentData.STATE_START_BIT1Layer A EOF संकेतनएक मल्टी-सेगमेंट संदेश का पहला सेगमेंट
BleSegmentData.STATE_END_BIT2Layer A EOF संकेतनअंतिम सेगमेंट (या एकल सेगमेंट)

डिज़ाइन ट्रेड-ऑफ़ सारांश {#design-trade-offs-recap}

Diagram 14
14

आगे पढ़ने के लिए

  • Chat ArchitectureBleTransport LAN → Aware → BLE फ़ॉलबैक श्रृंखला और व्यापक चैट भेजना/प्राप्त करना पाइपलाइन में कैसे फिट बैठता है।
  • Pairing Flow — हर BLE पेलोड द्वारा उपयोग की जाने वाली साझा ChaCha20 कुंजी कैसे स्थापित होती है, और पेयरिंग हैंडशेक के लिए NEARBY characteristic का उपयोग कैसे होता है।