ব্লগে ফিরে যান
Transport15 min read

BLE ট্রান্সপোর্ট ডিজাইন — বার্তা ও ফাইল ডাউনলোড

এই নিবন্ধে ব্যাখ্যা করা হয়েছে কীভাবে PlainApp Bluetooth Low Energy-এর মাধ্যমে চ্যাট বার্তা পাঠায় এবং ফাইল ডাউনলোড করে যখন LAN বা Wi-Fi Aware কোনোটিই উপলব্ধ নয়। BLE হলো গ্যারান্টিড ফলব্যাক: ধীর, কিন্তু কোনো IP কানেক্টিভিটি ছাড়াই এটি কাজ করে। নিবন্ধটি ওয়্যার ফরম্যাট, দ্বি-স্তর চাঙ্কিং ডিজাইন, কনকারেন্ট ট্রাফিক কীভাবে অগ্রাধিকার দেওয়া হয় (এবং দেওয়া হয় না), এবং কেন প্রতিটি রিকোয়েস্টের পর প্রতিটি সংযোগ ভেঙে দেওয়া হয় তা কভার করে।

এই ট্রান্সপোর্ট গ্রহণ করে এমন বৃহত্তর চ্যাট আর্কিটেকচারের জন্য Chat Architecture দেখুন। দুটি ডিভাইস কীভাবে প্রতিটি BLE পেলোড এনক্রিপ্ট করতে ব্যবহৃত শেয়ার্ড ChaCha20 কী প্রাপ্ত করে, তার জন্য Pairing Flow দেখুন।

সূচিপত্র

BLE ট্রান্সপোর্ট কেন আদৌ? {#why-a-ble-transport-at-all}

PlainApp সার্ভারবিহীন এবং অফলাইন-ফার্স্ট। ট্রান্সপোর্ট লেয়ার একটি অর্ডারড ফলব্যাক চেইন: LAN → Wi-Fi Aware → BLE। LAN হলো হ্যাপি পাথ (Wi-Fi-এর উপর HTTPS, ~১০ ms রাউন্ড ট্রিপ)। Wi-Fi Aware ক্রস-সাবনেট পিয়ারদের কভার করে (ভিন্ন SSID, গেস্ট বনাম IoT VLAN)। উভয়েরই কোনো না কোনো IP কানেক্টিভিটি প্রয়োজন। BLE একমাত্র ট্রান্সপোর্ট যা কাজ করে:

  • যখন ডিভাইসগুলো একই IP নেটওয়ার্কে একেবারেই নেই।
  • যখন Wi-Fi বন্ধ থাকে বা এয়ারপ্লেন মোডে থাকে (BLE রেডিও আলাদা)।
  • যখন Wi-Fi Aware অসমর্থিত (Android < 13, PlainApp-এর সমস্ত iOS ভ্যারিয়েন্ট)।

BLE ধীর — প্রতি সেকেন্ডে কয়েক ডজন KB, প্রতি রিকোয়েস্টে কয়েক সেকেন্ড লেটেন্সি — কিন্তু এটি যেকোনো পেয়ার্ড পিয়ারের জন্য গ্যারান্টিড, কারণ এর প্রয়োজনীয় একমাত্র জিনিস হলো পিয়ারের clientId, যা সর্বদা BLE স্ক্যান রেসপন্সে ব্রডকাস্ট করা হয়।

Diagram 1
1

GATT সার্ভিস লেআউট {#gatt-service-layout}

PlainApp দুটি ক্যারেক্টারিস্টিক সহ একটি একক কাস্টম GATT সার্ভিস বিজ্ঞাপন দেয়। এখানে কোনো নিবন্ধিত 16-bit UUID নেই — সার্ভিসটি একটি 128-bit UUID ব্যবহার করে যার ট্রেইলিং বাইটগুলো ASCII-ডিকোড করলে plpai\x01 হয়:

Diagram 2
2

কেন দুটি ক্যারেক্টারিস্টিক?

দুটি প্রোটোকলের ট্রাস্ট মডেল এবং পেলোড শেপ সম্পূর্ণ আলাদা:

  • NEARBY পেয়ারিং বার্তা বহন করে। এগুলো পিয়ার পেয়ার হওয়ার আগে আসে (এখনো কোনো শেয়ার্ড কী নেই), তাই এগুলো নিজস্ব প্রিফিক্স রাউটিং সহ নিজস্ব Ed25519-স্বাক্ষরিত JSON পেলোড ব্যবহার করে। বডিটি একটি প্লেইন স্ট্রিং।
  • HTTP পেয়ারিং-পরবর্তী সমস্ত ট্রাফিক (চ্যাট, ফাইল, প্রেজেন্স) বহন করে। এটি সর্বদা শেয়ার্ড কী দিয়ে ChaCha20-এনক্রিপ্টেড এবং LAN Ktor সার্ভারের মতো একই HttpRouteRegistry ব্যবহার করে, তাই রাউট হ্যান্ডলারগুলো (/peer_graphql, /fs, /peer_status) একবার লেখা হয় এবং উভয় ট্রান্সপোর্টের জন্য পুনঃব্যবহৃত হয়।

রিডের পরিবর্তে কেন নোটিফিকেশন?

BLE ATT প্রোটোকল একটি একক অ্যাট্রিবিউট রিড ৫১২ বাইট-এ সীমাবদ্ধ করে। একটি GraphQL রেসপন্স বা ১৬ KB ফাইল চাঙ্ক অনেক বড় হতে পারে। PlainApp এটি এড়াতে বাস্তব ডেটার জন্য readCharacteristic কখনো ব্যবহার না করে — সার্ভারের onCharacteristicReadRequestGATT_SUCCESS সহ একটি খালি পেলোড ফেরত দেয়। এর পরিবর্তে, ক্লায়েন্ট তার রিকোয়েস্ট ক্যারেক্টারিস্টিকে লেখে, এবং সার্ভার চাঙ্কড নোটিফিকেশনের একটি সিকোয়েন্স পাঠিয়ে সাড়া দেয় যা ক্লায়েন্ট পুনরায় একত্রিত করে। এটি BleDeviceApi.requestAsync, BleServerProtocol.handleWrite, এবং AndroidBleGattServer.sendChunkedResponse-এ নথিভুক্ত করা হয়েছে।

পিয়ার আইডেন্টিফিকেশন: shortId, MAC নয় {#peer-identification-shortid-not-mac}

BLE বিজ্ঞাপন প্যাকেটগুলো ক্ষুদ্র (৩১ বাইট) এবং BLE MAC অ্যাড্রেস Android দ্বারা প্রতি ~১৫ মিনিটে র্যান্ডমাইজ করা হয় — তাই এটি একটি স্থিতিশীল আইডেন্টিফায়ার হিসেবে ব্যবহার করা যায় না। PlainApp এর পরিবর্তে স্ক্যান রেসপন্সে একটি ৯-বাইট serviceData পেলোড ব্রডকাস্ট করে:

Diagram 3
3

সম্পূর্ণ clientId-এর পরিবর্তে ট্রাঙ্কেটেড হ্যাশ কেন?

একটি ১৩-অক্ষরের clientId ১৩ বাইটে ফিট করবে, কিন্তু PlainApp দুটি কারণে একটি ৮-বাইট ট্রাঙ্কেটেড SHA-256 বেছে নেয়:

১. স্থিতিশীল বাইট বাজেট। মোট ৯ বাইট ৩১-বাইট বিজ্ঞাপন পেলোডে সার্ভিস UUID (১৬ বাইট), দৈর্ঘ্য এবং টাইপ ফিল্ডের পাশাপাশি আরামদায়কভাবে ফিট হয় (~২৭ বাইট ব্যবহৃত, ৪ বাইট হেডরুম)। ২. প্রাইভেসি। BLE স্ক্যান করা একজন প্যাসিভ অবজারভার shortId থেকে clientId পুনরুদ্ধার করতে পারে না (একটি SHA-256 হ্যাশের ৮-বাইট প্রিফিক্স বাস্তবে অপরিবর্তনীয়)। তারা শুধুমাত্র এমন একজন পিয়ারকে চিনতে পারে যাকে তারা আগে একই shortId বিজ্ঞাপন করতে দেখেছে — তারা PlainApp ব্যবহারকারীদের এনিউমারেট করতে পারে না।

সম্পূর্ণ clientId শুধুমাত্র সেই পিয়ারকে প্রকাশ করা হয় যা আসলে GATT-এর মাধ্যমে সংযুক্ত হয়েছে এবং একটি DDiscoverReply বিনিময় করেছে — অর্থাৎ এমন একজন পিয়ার যার সাথে ব্যবহারকারী ইতিমধ্যে ইন্টারঅ্যাক্ট করতে বেছে নিয়েছেন।

দ্বি-স্তর চাঙ্কিং ডিজাইন {#two-layer-chunking-design}

এটি BLE ট্রান্সপোর্টের সবচেয়ে সূক্ষ্ম অংশ, এবং উভয় স্তর বোঝা অপরিহার্য কারণ এদের আকার এবং উদ্দেশ্য সম্পূর্ণ আলাদা:

Diagram 4
4

কেন ৩৮০ অক্ষর?

নেগোশিয়েটেড ATT MTU Android-এ ৫১৭ বাইট (requestMtu(517) — BLE স্পেসিফিকেশন দ্বারা অনুমোদিত সর্বোচ্চ) এবং iOS-এ ~১৮৫+ (CoreBluetooth দ্বারা অটো-নেগোশিয়েটেড)। ATT হেডার (~৩ বাইট) এবং BleSegmentData-এর JSON র‍্যাপার ওভারহেড ({"d":"...","s":N} ~১২ বাইট যোগ করে) বাদ দিলে, ৩৮০ অক্ষরের পেলোড উভয় প্ল্যাটফর্মে একটি একক ATT MTU-তে আরামদায়কভাবে ফিট হয়। মানটি সিমেট্রিক (ক্লায়েন্ট রিকোয়েস্ট ফ্র্যাগমেন্ট এবং সার্ভার নোটিফিকেশন ফ্র্যাগমেন্ট উভয়ই ৩৮০ ব্যবহার করে), যা কোড সরল রাখে।

ফাইল চাঙ্কের জন্য কেন ১৬ KiB?

একটি ১৬ KiB ফাইল চাঙ্ক base64-এনকোড হলে ~২২ KiB JSON হয়, যা ~৫৮টি GATT নোটিফিকেশন সেগমেন্টে বিভক্ত হয়। প্রতিটি requestAsync রাউন্ড-ট্রিপ BLE-এর উপর কয়েক সেকেন্ড সময় নেয়, তাই কম-কিন্তু-বড় চাঙ্ক প্রতি-চাঙ্ক ওভারহেড কমায়। আরও বড় করলে BLE RPC টাইমআউটে পৌঁছানোর ঝুঁকি থাকবে এবং খারাপ প্রোগ্রেস ফিডব্যাক পাওয়া যাবে (ব্যবহারকারী প্রতি চাঙ্কে একবারই প্রোগ্রেস আপডেট দেখেন)। ১৬ KiB হলো অভিজ্ঞতামূলকভাবে টিউন করা সুইট স্পট — থ্রুপুটের জন্য যথেষ্ট বড়, রেসপন্সিভ প্রোগ্রেস UI-এর জন্য যথেষ্ট ছোট।

RPC প্রিমিটিভ: BleDeviceApi.requestAsync {#the-rpc-primitive-bledeviceapirequestasync}

প্রতিটি BLE চ্যাট বার্তা এবং প্রতিটি ফাইল চাঙ্ক হলো BleDeviceApi.requestAsync(service, requestData)-এ একটি কল — একটি suspend ফাংশন যা একটি BleResult ফেরত দেয়। কলারের দৃষ্টিকোণ থেকে এটি সিঙ্ক্রোনাস: একটি রিকোয়েস্ট → একটি সম্পূর্ণ পুনরায় একত্রিত রেসপন্স, কোনো পাইপলাইনিং নেই।

Diagram 5
5

মূল ইনভেরিয়েন্ট

১. একটি রিকোয়েস্ট → একটি রেসপন্স। requestAsync কলারের দৃষ্টিকোণ থেকে সিঙ্ক্রোনাস — এটি সম্পূর্ণ রেসপন্স পুনরায় একত্রিত হওয়ার পরেই ফেরত আসে। কোনো পাইপলাইনিং নেই। ২. প্রতি-কলে নোটিফিকেশন এনেবলড। ক্লায়েন্ট প্রতিটি requestAsync-এর শুরুতে CCCD লেখে এবং শেষে এটি ডিসেবল করে। এটি অপচয়ী (প্রতি কলে দুটি অতিরিক্ত GATT রাইট) কিন্তু প্রোটোকলটি স্টেটলেস রাখে — সার্ভারকে ট্র্যাক করতে হয় না কোন ক্লায়েন্টগুলো "লিসেনিং" করছে। ৩. RPC-এর মধ্যে কোনো পুনঃপ্রচেষ্টা নেই। যদি একটি মাত্র writeCharacteristic টাইমআউট হয় (৫ সেকেন্ড), সম্পূর্ণ RPC বাতিল হয়ে যায়। শুধুমাত্র ensureConnected পুনঃপ্রচেষ্টা করে (কানেক্ট ব্যর্থতায় ৩টি প্রচেষ্টা)। কোর্স ট্রান্সপোর্ট-লেভেল ব্যাকঅফ 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 এনভেলপ টেক্সট এবং বাইনারি উভয় পেলোড বহন করে।

বার্তা সেন্ড পাথ (এন্ড-টু-এন্ড) {#message-send-path-end-to-end}

সব একত্রিত করে — যখন একটি চ্যাট বার্তা BLE-এর মাধ্যমে পাঠানো হয় তখন কী হয়:

Diagram 7
7

উল্লেখযোগ্য ডিজাইন পছন্দ

  • LAN-এর মতো একই কী। পেয়ারিং থেকে প্রাপ্ত ChaCha20 শেয়ার্ড কী BLE-এর জন্য পুনঃব্যবহৃত হয় — কোনো আলাদা BLE কী নেই। LanTransport দ্বারা ব্যবহৃত OkHttp ক্রিপ্টো ইন্টারসেপ্টর এবং BleTransport-এ ম্যানুয়াল chaCha20Encrypt/chaCha20Decrypt একই প্রিমিটিভ, শুধু ভিন্নভাবে ইনভোক করা হয়েছে।
  • LAN-এর মতো একই রাউট হ্যান্ডলার। BleHttpRequestHttpRouteRegistry.matchRoute(path)-এর মাধ্যমে ডিসপ্যাচ করা হয়, যে রেজিস্ট্রিটি Ktor LAN সার্ভার ব্যবহার করে। তাই /peer_graphql, /fs, /peer_status ইত্যাদি ঠিক একবার ইমপ্লিমেন্ট করা হয় এবং উভয় ট্রান্সপোর্টে অভিন্নভাবে কাজ করে।
  • কোনো সংযোগ পুনঃব্যবহার নেই। finally { scanner.teardownConnection(client) } ব্লকটি সর্বদা চলে। প্রতিটি বার্তা সম্পূর্ণ connect→discoverServices→MTU খরচ (~কয়েক সেকেন্ড) বহন করে। এটি একটি ইচ্ছাকৃত ট্রেড-অফ — Design Trade-offs দেখুন।

ফাইল ডাউনলোড পাথ (এন্ড-টু-এন্ড) {#file-download-path-end-to-end}

BLE-এর উপর ডাউনলোডগুলো স্ট্রিমিং — ফাইলটি ১৬ KiB চাঙ্কে পড়া হয় এবং এটি আসার সাথে সাথে একটি টেম্প ফাইলে লেখা হয়, তাই একটি ১০ MB ফাইলের জন্য ১০ MB RAM-এর প্রয়োজন হয় না। কৌশলটি হলো প্রতিটি চাঙ্কের RPC একটি আলাদা requestAsync কল, এবং চাঙ্কগুলো একটি ByteChannel-এ পুশ করা হয় যা কনজ্যুমার কনকারেন্টলি পড়ে।

Diagram 8
8

একটি বড় RPC-এর পরিবর্তে কেন স্ট্রিমিং?

একটি ১০ MB ফাইল একক RPC হিসেবে পাঠানো হলে ~২৮০ ০০০ নোটিফিকেশন সেগমেন্ট হবে, যা রেসপন্স শুরু হওয়ার আগেই উভয় পক্ষে মেমরিতে ধরে রাখতে হবে — এবং সম্পূর্ণ ট্রান্সফারটি কোনো প্রোগ্রেস রিপোর্ট করার আগেই সফল হতে হবে। আরও খারাপ, মাঝখানে একটি ড্রপ করা নোটিফিকেশন পুরো জিনিসটি নষ্ট করে দেবে।

চাঙ্কড ডিজাইনের তিনটি জয়:

১. কনস্ট্যান্ট মেমরি। একসাথে শুধুমাত্র একটি ১৬ KiB চাঙ্ক ইন ফ্লাইটে থাকে। ২. লাইভ প্রোগ্রেস। DownloadQueue.notifyProgressUpdate() প্রতি সেকেন্ডে ফায়ার করে, এবং UI একটি ডাউনলোড বার দেখায়। ৩. রেজিলিয়েন্স। একটি ব্যর্থ চাঙ্ক স্বাধীনভাবে পুনঃপ্রচেষ্টা করা যেতে পারে (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

প্র্যাকটিসে এটি কেন কাজ করে

চ্যাটকে "অগ্রাধিকার পাওয়া মনে হয়" এমন সেপারেশনটি স্ট্রাকচারাল:

১. চ্যাট সেন্ডগুলো DownloadQueue-এর মাধ্যমে যায় না। এগুলো সরাসরি PeerGraphQLClient → PeerTransportRouter → BleTransport.send দ্বারা ইস্যু করা হয়। তাই একটি চ্যাট বার্তা কখনো ফাইল ডাউনলোডের সারির পেছনে বসে না। ২. প্রতিটি BleTransport কল নিজস্ব GATT সংযোগ খোলে। একটি দীর্ঘস্থায়ী ডাউনলোড যা একটি সংযোগ ধরে রেখেছে তা একই পিয়ারের সাথে দ্বিতীয় সংযোগ খোলার জন্য একটি চ্যাট সেন্ডকে বাধা দেয় না। Android একাধিক সিমুলটেনিয়াস GATT সংযোগ সমর্থন করে। ৩. চ্যাট RPC গুলো ছোট। একটি একক চ্যাট বার্তা হলো একটি requestAsync রাউন্ড ট্রিপ (কানেক্টের পর ~১ সেকেন্ড)। রেডিও ডাউনলোডের সাথে ব্যস্ত থাকলেও, চ্যাট সেন্ড কয়েক সেকেন্ডের মধ্যে সম্পন্ন হয়।

ডিজাইন যেখানে ঘাটতি দেখায়

"কোনো স্পষ্ট অগ্রাধিকার নেই"-এর ট্রেড-অফগুলো:

  • কানেক্ট লেটেন্সি। উভয় চ্যাট এবং ডাউনলোড প্রতিবার connect→discover→MTU খরচ (~কয়েক সেকেন্ড) বহন করে, কারণ সংযোগ পুনঃব্যবহৃত হয় না। ডাউনলোডের সময় আসা একটি চ্যাট বার্তা ডাউনলোডের বিদ্যমান সংযোগে পিগব্যাক করতে পারে না — এটি একটি নতুন খোলে।
  • Android-এ স্ট্যাটিক সারি। AndroidBleGattClient-এর প্রসেস-ওয়াইড operationQueue সমস্ত পিয়ার এবং সমস্ত সংযোগ জুড়ে GATT অপ্স সিরিয়ালাইজ করে। তাই দুটি GATT সংযোগ সহাবস্থান করতে পারলেও, তাদের write/read/notify অপারেশন সারি লেভেলে ইন্টারলিভ করা হয়। প্র্যাকটিসে এটি ঠিক আছে (প্রতিটি অপ ~ms) কিন্তু উচ্চ কনকারেন্সির অধীনে এটি একটি সূক্ষ্ম গ্লোবাল বটলনেক।
  • কোনো প্রিএম্পশন নেই। চলমান একটি ডাউনলোডকে একটি চ্যাট বার্তা যেতে দেওয়ার জন্য বিরতি দেওয়া যায় না। চ্যাট সেন্ড সহজভাবে কনকারেন্টলি চলে এবং রেডিও সময়ের জন্য প্রতিযোগিতা করে।

ভবিষ্যতের একটি উন্নতি হতে পারে BleTransport.send এবং downloadFile-এর চারপাশে একটি পার-পিয়ার Mutex, এবং সারিতে একটি অগ্রাধিকার ফিল্ড — কিন্তু বর্তমান ডিজাইন এই সত্যের উপর নির্ভর করে যে চ্যাট RPC গুলো যথেষ্ট ছোট যে কনটেনশন কদাচিৎ ব্যবহারকারী-দৃশ্যমান।

কনকারেন্সি কন্ট্রোল ও স্ট্যাটিক GATT সারি {#concurrency-control--the-static-gatt-queue}

এটি নিজস্ব সেকশনের যোগ্য কারণ এটি 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 যার কোনো মিউটেক্স, কোনো সারি, কোনো পার-পিয়ার সিরিয়ালাইজেশন নেই। একই পিয়ারের জন্য BleTransport.send-এ দুটি কনকারেন্ট কল প্রত্যেকে নিজস্ব GATT সংযোগ খুলবে এবং স্বাধীনভাবে এগিয়ে যাবে। সিরিয়ালাইজেশন স্পষ্টভাবে GATT অপারেশন লেভেলে ঘটে (Android-এ স্ট্যাটিক সারির মাধ্যমে, বা iOS-এ সিকোয়েন্সিয়াল await-এর মাধ্যমে)।

সংযোগ লাইফসাইকেল ও MTU নেগোশিয়েশন {#connection-lifecycle--mtu-negotiation}

Diagram 11
11

কেন requestMtu(517)?

ডিফল্ট ATT MTU হলো ২৩ বাইট (৩-বাইট ATT হেডারের পর পেলোডের শুধুমাত্র ২০ বাইট)। ডিফল্ট MTU সহ, প্রতিটি ৩৮০-অক্ষর সেগমেন্টের জন্য ১টির পরিবর্তে ~১৯টি GATT রাইট প্রয়োজন হবে — একটি ১৯× ধীরগতি। BLE স্পেক দ্বারা অনুমোদিত সর্বোচ্চ MTU (৫১৭ বাইট) অনুরোধ করলে ৩৮০-অক্ষর সেগমেন্টগুলো একটি একক ATT অপারেশনে ফিট হয়, থ্রুপুট নাটকীয়ভাবে উন্নত করে।

iOS কোনো স্পষ্ট MTU রিকোয়েস্ট API প্রকাশ করে না — CoreBluetooth সংযোগের সময় পেরিফেরালের সাথে এটি স্বয়ংক্রিয়ভাবে নেগোশিয়েট করে। মডার্ন iOS ডিভাইসগুলো সাধারণত ~১৮৫ বাইট নেগোশিয়েট করে, যা এখনও ৩৮০-অক্ষর সেগমেন্টগুলোর সাথে আরামদায়কভাবে ফিট হয় (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}

কনস্ট্যান্টValueকোথায়উদ্দেশ্য
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সংযোগ সেটআপসর্বোচ্চ ৪টি মোট প্রচেষ্টা (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পিয়ার আইডেন্টিফিকেশন১ ফ্ল্যাগ বাইট + ৮ shortId বাইট
BleSegmentData.STATE_START_BIT1Layer A EOF সিগন্যালিংএকটি মাল্টি-সেগমেন্ট বার্তার প্রথম সেগমেন্ট
BleSegmentData.STATE_END_BIT2Layer A EOF সিগন্যালিংশেষ সেগমেন্ট (বা একক সেগমেন্ট)

ডিজাইন ট্রেড-অফ সারাংশ {#design-trade-offs-recap}

Diagram 14
14

আরও পড়ার জন্য

  • Chat Architecture — BleTransport কীভাবে LAN → Aware → BLE ফলব্যাক চেইন এবং বৃহত্তর চ্যাট সেন্ড/রিসিভ পাইপলাইনে ফিট করে।
  • Pairing Flow — প্রতিটি BLE পেলোড দ্বারা ব্যবহৃত শেয়ার্ড ChaCha20 কী কীভাবে স্থাপন করা হয়, এবং NEARBY ক্যারেক্টারিস্টিক কীভাবে পেয়ারিং হ্যান্ডশেকের জন্য ব্যবহৃত হয়।