Bloga dön
Transport15 min read

BLE Taşıma Tasarımı — Mesajlar ve Dosya İndirmeleri

Bu makale, PlainApp'in ne LAN ne de Wi-Fi Aware kullanılabilir olduğunda Bluetooth Low Energy üzerinden sohbet mesajlarını nasıl gönderdiğini ve dosyaları nasıl indirdiğini açıklamaktadır. BLE, garanti edilen yedektir: yavaştır, ancak hiçbir IP bağlantısı olmadan çalışır. Makale, kablo biçimini, iki katmanlı parçalama tasarımını, eşzamanlı trafiğin nasıl önceliklendirildiğini (ve önceliklendirilmediğini) ve her bağlantının neden her istekten sonra yıkıldığını ele almaktadır.

Bu taşımayı kullanan daha geniş sohbet mimarisi için Sohbet Mimarisi makalesine bakın. İki cihazın her BLE yükünü şifrelemek için kullanılan paylaşılan ChaCha20 anahtarını nasıl elde ettiğini görmek için Eşleştirme Akışı makalesine bakın.

İçindekiler

Neden Bir BLE Taşıması? {#why-a-ble-transport-at-all}

PlainApp sunucusuz ve çevrimdışı önceliklidir. Taşıma katmanı, sıralı bir yedek zinciridir: LAN → Wi-Fi Aware → BLE. LAN mutlu yoldur (Wi-Fi üzerinden HTTPS, ~10 ms gidiş-dönüş). Wi-Fi Aware, alt ağ ötesi eşleri kapsar (farklı SSID'ler, misafir ve IoT VLAN'ları). Her ikisi de bir tür IP bağlantısı gerektirir. BLE, şu durumlarda çalışan tek taşımadır:

  • Cihazlar aynı IP ağında hiç değilse.
  • Wi-Fi kapalıysa veya uçak modundaysa (BLE radyosu ayrıdır).
  • Wi-Fi Aware desteklenmiyorsa (Android < 13, PlainApp'in tüm iOS varyantları).

BLE yavaştır — saniyede onlarca KB, istek başına saniyelerce gecikme — ancak herhangi bir eşleştirilmiş eş için garanti edilmiştir, çünkü ihtiyaç duyduğu tek şey, BLE tarama yanıtında her zaman yayınlanan eşin clientId'sidir.

Diagram 1
1

GATT Hizmet Düzeni {#gatt-service-layout}

PlainApp, iki karakteristiğe sahip tek bir özel GATT hizmeti tanıtır. Kayıtlı 16 bitlik UUID yoktur — hizmet, sondaki baytları ASCII olarak plpai\x01'e çözülen 128 bitlik bir UUID kullanır:

Diagram 2
2

Neden iki karakteristik?

İki protokolün tamamen farklı güven modelleri ve yük şekilleri vardır:

  • NEARBY, eşleştirme mesajlarını taşır. Bunlar, eş henüz eşleştirilmeden önce gelir (henüz paylaşılan anahtar yok), bu nedenle kendi önek yönlendirmesiyle kendi Ed25519 imzalı JSON yüklerini kullanırlar. Gövde düz bir metindir.
  • HTTP, eşleştirme sonrası tüm trafiği (sohbet, dosyalar, varlık) taşır. Her zaman paylaşılan anahtarla ChaCha20 ile şifrelidir ve LAN Ktor sunucusuyla aynı HttpRouteRegistry'i kullanır, böylece rota işleyicileri (/peer_graphql, /fs, /peer_status) bir kez yazılır ve her iki taşıma için yeniden kullanılır.

Neden okuma yerine bildirimler?

BLE ATT protokolü, tek bir öznitelik okumasını 512 bayt ile sınırlar. Bir GraphQL yanıtı veya 16 KB'lık bir dosya parçası çok daha büyük olabilir. PlainApp, gerçek veriler için asla readCharacteristic kullanmayarak bunu çözer — sunucunun onCharacteristicReadRequest'i GATT_SUCCESS ile boş bir yük döndürür. Bunun yerine, istemci isteğini karakteristiğe yazar ve sunucu, istemcinin yeniden birleştirdiği bir parçalı bildirim dizisi göndererek yanıt verir. Bu, BleDeviceApi.requestAsync, BleServerProtocol.handleWrite ve AndroidBleGattServer.sendChunkedResponse içinde belgelenmiştir.

Eş Tanımlama: shortId, MAC değil {#peer-identification-shortid-not-mac}

BLE reklam paketleri küçüktür (31 bayt) ve BLE MAC adresi Android tarafından yaklaşık 15 dakikada bir rastgeleleştirilir — bu nedenle kararlı bir tanımlayıcı olarak kullanılamaz. PlainApp bunun yerine tarama yanıtında 9 baytlık bir serviceData yükü yayınlar:

Diagram 3
3

Neden tam clientId yerine kırpılmış bir hash?

13 karakterlik bir clientId 13 bayta sığar, ancak PlainApp iki nedenle 8 baytlık kırpılmış bir SHA-256 seçer:

  1. Kararlı bayt bütçesi. Toplam 9 bayt, 31 baytlık reklam yüküne hizmet UUID'si (16 bayt), uzunluk ve tür alanlarıyla rahatça sığar (~27 bayt kullanıldı, 4 bayt boşluk).
  2. Gizlilik. BLE taraması yapan pasif bir gözlemci, shortId'den clientId'yi kurtaramaz (bir SHA-256 hash'inin 8 baytlık öneki pratikte geri döndürülemez). Yalnızca aynı shortId'yi daha önce yayınladığını gördükleri bir eşi tanıyabilirler — PlainApp kullanıcılarını sayamazlar.

Tam clientId, yalnızca GATT üzerinden gerçekten bağlanmış ve bir DDiscoverReply almış bir eşe — yani kullanıcının etkileşimde bulunmayı zaten seçtiği bir eşe açıklanır.

İki Katmanlı Parçalama Tasarımı {#two-layer-chunking-design}

Bu, BLE taşımasının en ince kısmıdır ve her iki katmanı da anlamak esaslidir çünkü tamamen farklı boyutlara ve amaçlara sahiptirler:

Diagram 4
4

Neden 380 karakter?

Müzakere edilen ATT MTU, Android'de 517 bayttır (requestMtu(517) — BLE spesifikasyonunun izin verdiği maksimum) ve iOS'ta ~185+ (CoreBluetooth tarafından otomatik müzakere edilir). ATT başlığı (~3 bayt) ve BleSegmentData'nın JSON sarmalayıcı yükü ({"d":"...","s":N} ~12 bayt ekler) çıkarıldığında, 380 karakterlik yük her iki platformda tek bir ATT MTU'ya rahatça sığar. Değer simetriktir (hem istemci istek parçaları hem de sunucu bildirim parçaları 380 kullanır), bu da kodu basit tutar.

Neden dosya parçaları için 16 KiB?

16 KiB'lik bir dosya parçası base64 ile ~22 KiB JSON'a kodlanır, bu da ~58 GATT bildirim segmentine parçalanır. BLE üzerinden her requestAsync gidiş-dönüşü saniyeler sürer, bu nedenle daha az ama daha büyük parçalar parça başına yükü azaltır. Çok daha büyütmek, BLE RPC zaman aşımlarına çarpmak ve kötü ilerleme geri bildirimi üretmek riskini doğurur (kullanıcı ilerlemeyi parça başına yalnızca bir kez güncellenmiş görür). 16 KiB, ampirik olarak ayarlanmış tatlı noktadır — verimlilik için yeterince büyük, yanıt veren ilerleme UI'si için yeterince küçük.

RPC İlkesi: BleDeviceApi.requestAsync {#the-rpc-primitive-bledeviceapirequestasync}

Her BLE sohbet mesajı ve her dosya parçası, bir BleResult döndüren bir suspend fonksiyonu olan BleDeviceApi.requestAsync(service, requestData) için bir çağrıdır. Çağırıcı açısından senkroniktir: bir istek → tamamen yeniden birleştirilmiş bir yanıt, ardışık işleme yok.

Diagram 5
5

Anahtar değişmezler

  1. Bir istek → bir yanıt. requestAsync, çağırıcı açısından senkroniktir — yalnızca tam yanıt yeniden birleştirildikten sonra döner. Ardışık işleme yoktur.
  2. Çağrı başına bildirimler etkin. İstemci, her requestAsync'in başında CCCD'yi yazar ve sonunda devre dışı bırakır. Bu israflıdır (çağrı başına iki ekstra GATT yazması) ancak protokolü durumsuz tutar — sunucu hangi istemcilerin «dinlediğini» takip etmek zorunda değildir.
  3. RPC içinde yeniden deneme yok. Tek bir writeCharacteristic zaman aşımına uğrarsa (5 s), tüm RPC iptal edilir. Yalnızca ensureConnected yeniden dener (bağlantı başarısızlığında 3 deneme). Kaba taşıma düzeyinde geri çekilme PeerCircuitBreaker tarafından sağlanır, RPC katmanı tarafından değil.

Kablo Zarf Biçimi {#wire-envelope-format}

A katmanı segmentlerinin içindeki yük, iç içe geçmiş bir JSON zarfıdır. Parçalamayı çıkarırsak, mantıksal yapı şöyledir:

Diagram 6
6

Yanıt şekli

Yanıt, aynı A katmanı parçalaması aracılığıyla ters yönde akar, ancak iç JSON üç alana sahip bir BleHttpResponse'dur: s (HTTP durum kodu), h (yanıt başlıkları haritası) ve b (gövde). Gövde, BleHttpCall.encodeResponse() tarafından her zaman base64 olarak kodlanır, boş olduğunda bile — yanıt ikili olabilir (şifreli GraphQL baytları, ham /fs dosya baytları) ve BLE taşıması yalnızca dize olduğu için, aynı JSON zarfı hem metin hem de ikili yükleri taşır.

Mesaj Gönderme Yolu (Uçtan Uca) {#message-send-path-end-to-end}

Hepsini bir araya getirirsek — BLE üzerinden bir sohbet mesajı gönderildiğinde ne olur:

Diagram 7
7

Önemli tasarım seçimleri

  • LAN ile aynı anahtar. Eşleştirmeden gelen ChaCha20 paylaşılan anahtarı BLE için yeniden kullanılır — ayrı bir BLE anahtarı yoktur. LanTransport tarafından kullanılan OkHttp şifreleme interceptor'ü ve BleTransport'teki manuel chaCha20Encrypt/chaCha20Decrypt aynı ilkedir, sadece farklı şekilde çağrılır.
  • LAN ile aynı rota işleyicileri. BleHttpRequest, Ktor LAN sunucusunun kullandığı aynı kayıt defteri olan HttpRouteRegistry.matchRoute(path) üzerinden gönderilir. Bu nedenle /peer_graphql, /fs, /peer_status vb. tam olarak bir kez uygulanır ve her iki taşıma üzerinden aynı şekilde çalışır.
  • Bağlantı yeniden kullanımı yok. finally { scanner.teardownConnection(client) } bloğu her zaman çalışır. Her mesaj tam bağlan→servisleriKeşfet→MTU maliyetini öder (~saniyeler). Bu kasıtlı bir takastır — bkz. Tasarım Takasları.

Dosya İndirme Yolu (Uçtan Uca) {#file-download-path-end-to-end}

BLE üzerinden indirmeler akışlıdır — dosya 16 KiB parçalar halinde okunur ve geldikçe geçici bir dosyaya yazılır, böylece 10 MB'lık bir dosya 10 MB RAM gerektirmez. Püf noktası, her parçanın RPC'sinin ayrı bir requestAsync çağrısı olması ve parçaların tüketici tarafından eşzamanlı olarak okunan bir ByteChannel'a itilmesidir.

Diagram 8
8

Neden tek büyük RPC yerine akış?

Tek bir RPC olarak gönderilen 10 MB'lık bir dosya, ~280 000 bildirim segmenti anlamına gelir, hepsi yanıttan önce bile her iki tarafta bellekte tutulur — ve tüm transferin herhangi bir ilerleme bildirilmeden önce başarılı olması gerekir. Daha kötüsü, ortada tek bir düşen bildirim tümünü bozabilir.

Parçalı tasarımın üç kazancı vardır:

  1. Sabit bellek. Aynı anda yalnızca bir 16 KiB'lik parça yoldadır.
  2. Canlı ilerleme. DownloadQueue.notifyProgressUpdate() her saniye çalışır ve UI bir indirme çubuğu gösterir.
  3. Dayanıklılık. Başarısız bir parça bağımsız olarak yeniden denenebilir (DownloadQueue görev düzeyinde duraklat/devam et/yeniden dene destekler; akış ortasında bir başarısızlık kısmi geçici dosyayı bırakır, ancak şu anda indirici başarısızlıkta onu siler — takaslara bakın).

onClose neden indirme işini iptal eder

DownloadedResponse.onClose callback'i downloadJob.cancel() çağırır. Bu esaslidir çünkü indirme döngüsü, tüketici kanalı erken terk ederse (örneğin kullanıcı Duraklat'a dokunduysa) aksi takdirde sonsuza kadar çalışmaya devam edecek bir alt coroutine'de çalışır. DownloadedResponse üzerindeki AutoCloseable sözleşmesi, tüketicinin use { ... } bloğunun çıkışta otomatik olarak onClose'u çağırması, BLE indirme coroutine'ini iptal etmesi ve coroutine'in finally bloğunda GATT bağlantısını yıkması anlamına gelir.

Önceliklendirme: Sohbet Pratikte Dosyaları Nasıl Geçer {#prioritization-how-chat-beats-files-in-practice}

Bu, herhangi bir sohbet uygulaması için en önemli sorudur: yavaş bir BLE dosya indirmesi devam ederken, yeni bir sohbet mesajı önüne geçebilir mi?

Dürüst cevap: açık bir öncelik şeması yoktur

BLE kodunda veya indirme kuyruğunda öncelik alanı, öncelik kuyruğu, önleme yoktur. Bunu kapsamlı grep ile doğruladım — shared/src'deki tek priority eşleşmeleri günlük öncelik seviyeleri ve EXIF meta verileridir, mesaj ve indirme sıralamasıyla ilgili hiçbir şey.

Bunun yerine var olan, istenen davranışı ortaya çıkan bir özellik olarak üreten bir dizi mimari ayrımdır:

Diagram 9
9

Neden pratikte çalışır

Sohbeti «öncelikli hissettiren» ayrım yapısal:

  1. Sohbet gönderimleri DownloadQueue'dan geçmez. Doğrudan PeerGraphQLClient → PeerTransportRouter → BleTransport.send tarafından verilir. Bu nedenle bir sohbet mesajı asla bir dosya indirme kuyruğunun arkasında oturmaz.
  2. Her BleTransport çağrısı kendi GATT bağlantısını açar. Bir bağlantıyı tutan uzun süren bir indirme, bir sohbet gönderiminin aynı eşe ikinci bir bağlantı açmasını engellemez. Android, eşzamanlı birden çok GATT bağlantısını destekler.
  3. Sohbet RPC'leri kısadır. Tek bir sohbet mesajı bir requestAsync gidiş-dönüşüdür (bağlantıdan sonra ~1 s). Radyo bir indirmeyle meşgul olsa bile, sohbet gönderimi birkaç saniye içinde tamamlanır.

Tasarımın yetersiz kaldığı yerler

«Açık öncelik yok»un takasları:

  • Bağlantı gecikmesi. Hem sohbet hem de indirme, bağlantılar yeniden kullanılmadığından her seferinde bağlan→keşfet→MTU maliyetini (~saniyeler) öder. Bir indirme sırasında gelen bir sohbet mesajı, indirmenin mevcut bağlantısına ortak olamaz — yeni bir tane açar.
  • Android'de statik kuyruk. AndroidBleGattClient'teki süreç geneli operationQueue, GATT işlemlerini tüm eşler ve tüm bağlantılar arasında seri hale getirir. Bu nedenle iki GATT bağlantısı bir arada var olsa da, yazma/okuma/bildirim işlemleri kuyruk düzeyinde iç içe geçer. Pratikte bu iyidir (her işlem ~ms'dir) ancak yüksek eşzamanlılık altında ince bir küresel darboğazdır.
  • Önleme yok. Devam eden bir indirme, bir sohbet mesajının geçmesine izin için duraklatılamaz. Sohbet gönderimi basitçe eşzamanlı çalışır ve radyo süresi için yarışır.

Gelecekteki bir iyileştirme, BleTransport.send ve downloadFile etrafında eş başına bir Mutex ve kuyrukta bir öncelik alanı olabilir — ancak mevcut tasarım, sohbet RPC'lerinin çekişmenin nadiren kullanıcı tarafından görünür olacak kadar kısa olduğu gerçeğine güvenir.

Eşzamanlılık Kontrolü ve Statik GATT Kuyruğu {#concurrency-control--the-static-gatt-queue}

Bu, kendi bölümünü hak eder çünkü Android BLE uygulamasının en ince yönüdür.

Diagram 10
10

Neden statik (süreç geneli)?

Android BLE yığını, tek bir BluetoothGatt örneğinde eşzamanlı GATT işlemlerine izin vermez — başka bir yazma devam ederken writeCharacteristic çağrısı false döndürür ve ikinci yazmayı sessizce bırakır. Standart geçici çözüm, BluetoothGatt başına bir kuyruktur. PlainApp bir adım ileri gider ve companion object'te süreç geneli bir kuyruk kullanır, bu aşırı muhafazakardır ama doğrudur: uygulamadaki hiçbir iki GATT işleminin aynı anda çalışmamasını garanti eder.

Maliyeti, uzun bir BLE dosya indirmesinin yazma/okuma/bildirim işlemlerinin başka herhangi bir eşin GATT işlemlerinin arkasına sıralanmasıdır (ve onlar da arkasına sıralanır). Her bir işlem ~ms olduğundan, bu nadiren kullanıcı tarafından görülebilen bir darboğazdır — ancak birden çok eşe ağır eşzamanlı BLE trafiği altında bir tane olabilir.

Taşıma katmanında eş başına kilit yok

BleDeviceApi.requestAsync, mutex'i, kuyruğu, eş başına serileştirme olmayan düz bir suspend fun'dur. Aynı eş için BleTransport.send'e yapılan iki eşzamanlı çağrı, her biri kendi GATT bağlantısını açar ve bağımsız olarak ilerler. Serileştirme, GATT işlem düzeyinde dolaylı olarak gerçekleşir (Android'de statik kuyruk aracılığıyla veya iOS'ta ardışık await aracılığıyla).

Bağlantı Yaşam Döngüsü ve MTU Müzakeresi {#connection-lifecycle--mtu-negotiation}

Diagram 11
11

Neden requestMtu(517)?

Varsayılan ATT MTU 23 bayttır (3 baytlık ATT başlığından sonra yalnızca 20 bayt yük). Varsayılan MTU ile, her 380 karakterlik segment ~19 GATT yazması yerine 1 gerektirir — 19× yavaşlama. BLE spesifikasyonunun izin verdiği maksimum MTU'yu (517 bayt) istemek, 380 karakterlik segmentlerin tek bir ATT işlemine sığmasını sağlar, verimliliği önemli ölçüde artırır.

iOS, açık bir MTU isteme API'si sunmaz — CoreBluetooth, bağlantı sırasında çevre birimle otomatik olarak müzakere eder. Modern iOS cihazları tipik olarak ~185 bayt müzakere eder, bu da yine 380 karakterlik segmentleri rahatça sığdırır (ATT başlığı + JSON sarmalayıcı yükü çıkarıldıktan sonra).

Bildirimler için Akış Kontrolü {#flow-control-for-notifications}

Sunucu, yanıt parçalarını bildirim olarak gönderir, ancak BLE bildirimlerinin yerleşik akış kontrolü yoktur — sunucu, denetleyicinin iletebileceğinden daha hızlı bildirim gönderirse, bunlar sessizce bırakılır. PlainApp, açık onay tabanlı akış kontrolü uygular:

Diagram 12
12

Bu akış kontrolü olmadan, arka arkaya bildirimler, BLE denetleyicisinin iç gönderme kuyruğu dolduğunda BleGattServer arayüzü yorumlarında belgelenen iyi bilinen bir Android BLE sorunu olarak sessizce bırakılır. Cihaz başına tek-yolda kuralı, her bildirimin ya iletildiğini ya da bir zaman aşımı tetiklediğini (daha sonra bir taşıma başarısızlığı olarak ele alınır) garanti eder.

Hata Yönetimi: TransportUnavailable ve Gerçek Başarısızlık {#error-handling-transportunavailable-vs-real-failure}

TransportUnavailable, PeerTransportRouter'a bir sonraki taşımaya geçmesini söyleyen sinyaldir. Başka her şey, çağırıcıya dönen gerçek bir başarısızlıktır.

Diagram 13
13

İndirme başarısızlığı inceliği

BleTransport.downloadFile, hemen DownloadedResponse(200, channel, onClose) döndürür — parçalı indirme döngüsü, kanala yazan bir arka plan coroutine'de çalışır. Bir parça RPC'si akış ortasında başarısız olursa, döngü channel.close(TransportUnavailable(...)) çağırır, bu da tüketici (PeerFileDownloader.downloadAsync) hatayı channel.readAvailable(buf)'den bir fırlatılan istisna olarak görür.

Bu, PeerTransportRouter.downloadFile çağrısının kendisinin başarılı olduğu (bir DownloadedResponse döndürdüğü) anlamına gelir, bu nedenle devre kesici, akış ortası indirme hataları için bir başarısızlık kaydetmez. Yalnızca bağlantı zamanı ve tarama zamanı başarısızlıkları yönlendirici tarafından yakalanır. Bu kasıtlı bir tasarım seçimidir — akış ortası bir başarısızlık, o eş için BLE'yi kalıcı olarak devre dışı bırakmamalıdır (eş yalnızca geçici olarak menzil dışına çıkmış olabilir).

Anahtar Sabitler Referansı {#key-constants-reference}

SabitDeğerNeredeAmaç
BleDeviceApi.CHUNK_SIZE380GATT segment parçalamaHer BleSegmentData.data'nın boyutu (JSON yükünden sonra ATT MTU içine sığar)
BleTransport.CHUNK_SIZE16 384 (16 KiB)Dosya indirme bayt aralığıHer /fs parça isteğinin boyutu
BleTransport.SCAN_TIMEOUT_MS10 000BLE taramascanner.findOne için zaman aşımı
BleDeviceApi.NOTIFY_TIMEOUT_MS15 000RPC yanıtırequestAsync'de bildirim başına bekleme
AndroidBleGattClient MTU517Bağlantı kurulumurequestMtu(517) — BLE spesifikasyonunun izin verdiği maksimum
AndroidBleGattClient bağlantı zaman aşımı10 000Bağlantı kurulumuSTATE_CONNECTED için bekleme
AndroidBleGattClient MTU zaman aşımı5 000Bağlantı kurulumuonMtuChanged için bekleme
AndroidBleGattClient yazma zaman aşımı5 000GATT yazmaonCharacteristicWrite için bekleme
AndroidBleGattClient okuma zaman aşımı10 000GATT okumaonCharacteristicRead için bekleme (gerçek veriler için kullanılmaz)
AndroidBleGattClient bildirim-durumu zaman aşımı5 000CCCD yazmaCCCD deskriptör yazması için bekleme
ensureConnected yeniden deneme3Bağlantı kurulumuToplam 4 denemeye kadar (0..3)
AndroidBleGattServer.NOTIFY_ACK_TIMEOUT_MS10 000Bildirim akış kontrolüonNotificationSent için bekleme
AndroidBleGattServer notifyChunkSize380Yanıt parçalamaBleDeviceApi.CHUNK_SIZE ile aynı
IosBleGattServer yeniden deneme sınırı10Bildirim akış kontrolüVazgeçmeden önce maksimum updateValue yeniden deneme
PeerCircuitBreaker.WINDOW_MS30 000Taşıma devre kesiciEşik sonrası açık süre
PeerCircuitBreaker.MAX_FAILURES2Taşıma devre kesiciAçmak için pencere içinde başarısızlık
DownloadQueue.MAX_CONCURRENT3İndirme worker havuzuEşzamanlı indirme coroutine'leri
BleServiceData.SHORT_ID_BYTES8Eş tanımlamaKırpılmış SHA256 önek baytları
BleServiceData.PAYLOAD_BYTES9Eş tanımlama1 bayrak baytı + 8 shortId baytı
BleSegmentData.STATE_START_BIT1A katmanı EOF sinyaliÇok segmentli mesajın ilk segmenti
BleSegmentData.STATE_END_BIT2A katmanı EOF sinyaliSon segment (veya tek segment)

Tasarım Takasları Özeti {#design-trade-offs-recap}

Diagram 14
14

Daha Fazla Okuma

  • Sohbet Mimarisi — BleTransport'in LAN → Aware → BLE yedek zincirine ve daha geniş sohbet gönderme/alma ardışık düzenine nasıl uyduğu.
  • Eşleştirme Akışı — her BLE yükü tarafından kullanılan paylaşılan ChaCha20 anahtarının nasıl kurulduğu ve eşleştirme el sıkışması için NEARBY karakteristiğinin nasıl kullanıldığı. için.