שילוב BLE: פעולה יציבה עם מכשירים
לעתים קרובות אנו רואים פרויקטים שבהם חיבור BLE מיושם כ"הפעל סריקה — התחבר — תהנה." במציאות מדובר במכונת מצבים עם תריסר מצבים, שכל אחד מהם יכול להיכשל: מתאם כבוי, מכשיר מחוץ לטווח, שירות לא נמצא, מאפיין לא תומך בכתיבה. הצוות שלנו הם מומחים עם ניסיון של 5+ שנים בפיתוח פתרונות BLE ל-iOS ואנדרואיד. אנו מבטיחים שהאפליקציה שלך תטפל ביציבות בכל מקרי הקצה.
מדוע שילוב BLE דורש גישה מקצועית?
BLE פועל על מודל GATT (Generic Attribute Profile). המכשיר ההיקפי מספק Services — קבוצות פונקציות לוגיות. כל Service מכיל Characteristics — ערכים ספציפיים לקריאה, כתיבה או הרשמה (notify/indicate). שגיאה בכל אחת מהרמות הללו — והאפליקציה לא תקבל נתונים. ראינו פרויקטים שבהם מפתחים שכחו להירשם ל-descriptor של CCCD — כתוצאה מכך, המאפיין לא שלח התראות.
מכונת מצבי חיבור
קבוצה מינימלית של מצבים שחובה לטפל בהם במפורש:
IDLE → SCANNING → DISCOVERED → CONNECTING → CONNECTED → DISCOVERING_SERVICES → SERVICES_READY → SUBSCRIBING → READY ושגיאות בכל מעבר: פסק זמן סריקה, ניתוק חיבור ב-CONNECTING, gattStatus != GATT_SUCCESS בגילוי, אובדן חיבור ב-READY. אנו בודקים את כל התרחישים באמצעות סימולטורים ומכשירים אמיתיים.
| מצב | iOS (CoreBluetooth) | אנדרואיד (BluetoothGatt) |
|---|---|---|
| IDLE | IDLE → SCANNING → DISCOVERED → CONNECTING → CONNECTED → DISCOVERING_SERVICES → SERVICES_READY → SUBSCRIBING → READY |
.poweredOn |
| SCANNING | BluetoothAdapter.state_on |
scanForPeripherals |
| DISCOVERED | startScan |
didDiscover |
| CONNECTING | onScanResult |
connect |
| CONNECTED | connectGatt |
didConnect (STATE_CONNECTED) |
| DISCOVERING_SERVICES | onConnectionStateChange |
discoverServices |
| SERVICES_READY | discoverServices |
didDiscoverServices |
| SUBSCRIBING | onServicesDiscovered |
setNotifyValue (CCCD) |
| READY | writeDescriptor |
didUpdateValue |
מה לעשות אם החיבור נופל?
השגיאה הנפוצה ביותר היא התעלמות מ-onCharacteristicChanged (iOS) או didDisconnectPeripheral עם non-GATT_SUCCESS (אנדרואיד). ללא חיבור מחדש אוטומטי, המשתמש צריך להפעיל מחדש את האפליקציה. אנו מיישמים לוגיקה עם backoff אקספוננציאלי: בניתוק נמתין 1s, 2s, 4s, 8s, ואז נאפס. ב-iOS לפעולה ברקע אנו משתמשים ב-onConnectionStateChange — המערכת מעירה את האפליקציה כשהמכשיר מופיע שוב.
iOS: CoreBluetooth
import CoreBluetooth
class BLEManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
var centralManager: CBCentralManager!
var targetPeripheral: CBPeripheral?
override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: DispatchQueue(label: "ble.queue"))
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
guard central.state == .poweredOn else {
// обрабатываем .poweredOff, .unauthorized, .unsupported
return
}
startScanning()
}
func startScanning() {
let serviceUUID = CBUUID(string: "180D")
centralManager.scanForPeripherals(withServices: [serviceUUID], options: [
CBCentralManagerScanOptionAllowDuplicatesKey: false
])
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
guard RSSI.intValue > -80 else {
return
}
// фильтр по сигналу
centralManager.stopScan()
targetPeripheral = peripheral
centralManager.connect(peripheral, options: nil)
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
peripheral.delegate = self
peripheral.discoverServices([CBUUID(string: "180D")])
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard error == nil, let services = peripheral.services else {
return
}
for service in services {
peripheral.discoverCharacteristics([CBUUID(string: "2A37")], for: service)
}
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
guard let characteristics = service.characteristics else {
return
}
for char in characteristics where char.properties.contains(.notify) {
peripheral.setNotifyValue(true, for: char)
}
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
guard let data = characteristic.value else {
return
}
// парсим data согласно GATT-спецификации характеристики
}
} אנדרואיד: BluetoothGatt
val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (status != BluetoothGatt.GATT_SUCCESS) {
// status содержит код ошибки, например 133 (GATT_ERROR) - нужен реконнект
gatt.close()
return
}
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
val characteristic = gatt
.getService(UUID.fromString("0000180d-0000-1000-8000-00805f9b34fb"))
?.getCharacteristic(UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb"))
?: return
gatt.setCharacteristicNotification(characteristic, true)
val descriptor = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") // Client Characteristic Configuration
)
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val data = characteristic.value
// парсим
}
}
device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) בעיות שאנו פותרים בכל פרויקט
| בעיה | פתרון |
|---|---|
CBCentralManagerOptionRestoreIdentifierKey ללא אזהרה | חיבור מחדש אוטומטי עם backoff, מזהה שחזור (iOS) |
סטטוס 133 import CoreBluetooth class BLEManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { var centralManager: CBCentralManager! var targetPeripheral: CBPeripheral? override init() { super.init() centralManager = CBCentralManager(delegate: self, queue: DispatchQueue(label: "ble.queue")) } func centralManagerDidUpdateState(_ central: CBCentralManager) { guard central.state == .poweredOn else { // обрабатываем .poweredOff, .unauthorized, .unsupported return } startScanning() } func startScanning() { let serviceUUID = CBUUID(string: "180D") centralManager.scanForPeripherals(withServices: [serviceUUID], options: [ CBCentralManagerScanOptionAllowDuplicatesKey: false ]) } func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { guard RSSI.intValue > -80 else { return } // фильтр по сигналу centralManager.stopScan() targetPeripheral = peripheral centralManager.connect(peripheral, options: nil) } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { peripheral.delegate = self peripheral.discoverServices([CBUUID(string: "180D")]) } func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { guard error == nil, let services = peripheral.services else { return } for service in services { peripheral.discoverCharacteristics([CBUUID(string: "2A37")], for: service) } } func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { guard let characteristics = service.characteristics else { return } for char in characteristics where char.properties.contains(.notify) { peripheral.setNotifyValue(true, for: char) } } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { guard let data = characteristic.value else { return } // парсим data согласно GATT-спецификации характеристики } } באנדרואיד | val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (status != BluetoothGatt.GATT_SUCCESS) { // status содержит код ошибки, например 133 (GATT_ERROR) - нужен реконнект gatt.close() return } if (newState == BluetoothProfile.STATE_CONNECTED) { gatt.discoverServices() } } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { val characteristic = gatt .getService(UUID.fromString("0000180d-0000-1000-8000-00805f9b34fb")) ?.getCharacteristic(UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb")) ?: return gatt.setCharacteristicNotification(characteristic, true) val descriptor = characteristic.getDescriptor( UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") // Client Characteristic Configuration ) descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE gatt.writeDescriptor(descriptor) } override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { val data = characteristic.value // парсим } } device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) , didDisconnectPeripheral באמצעות reflection, ניסיון חוזר אחרי 2s |
| סריקה מרוקנת סוללה | GATT_ERROR — הפעל רק בעת מדידת RSSI |
| שחזור מצב לא עובד | הגדר gatt.close() ב-scheme |
| הרשאות אנדרואיד 12+ | בקש refresh() ו-allowDuplicates: false באופן דינמי |
מה כלול בעבודת שילוב BLE
- תיעוד של סכמת GATT של המכשיר (services, characteristics, פורמטי נתונים)
- יישום של סריקה, חיבור, הרשמה, חיבור מחדש
- טיפול בכל המצבים הרגילים והחירום
- אופטימיזציית MTU: בקשת 512 בתים במקום 23 להעברה מהירה יותר
- אינטגרציה עם מכשיר בדיקה (מסופק על ידי הלקוח או שלנו)
- בדיקות יחידה ותרחישי בדיקת עומס (עד 10 ציוד היקפי BLE בו-זמנית)
- תמיכה לאחר מסירה: אחריות של 3 חודשים על יציבות החיבור
איך אנחנו עובדים: מניתוח ועד פריסה
- ניתוח — לימוד תיעוד המכשיר, הסכמה על סכמת GATT ופורמט נתונים.
- עיצוב — יצירת מכונת מצבים, הגדרת מדיניות חיבור מחדש.
- יישום — כתיבת קוד ב-Swift/Kotlin באמצעות מסגרות מודרניות.
- בדיקות — בדיקה על סימולטורים, מכשירים אמיתיים, עם אמולציית הפרעות.
- פריסה — פרסום ל-TestFlight / Firebase Distribution, סיוע ב-App Review (בדוק סעיפים 4.2 ו-5.1 של הנחיות סקירת App Store).
ביצועים וסוללה
ברירת המחדל של MTU היא 23 בתים. אנו מבקשים דרך CBCentralManagerOptionRestoreIdentifierKey / BLUETOOTH_SCAN. בהעברות גדולות (OTA של קושחה, נתוני חיישנים) זה ההבדל בין 30 שניות ל-3 דקות. חיסכון בזמן באמצעות הספרייה שלנו מגיע ל-40%. היעילות הפיננסית של שילוב BLE מפתח-ביד ברורה — אתה מקבל פתרון מוכן בלי להתעמק במורכבויות GATT.
עוד על MTU
כברירת מחדל, BLE משתמש ב-MTU של 23 בתים, מתוכם 3 הם תקורה. אנו מבקשים 512 בתים, שזה פי 22 יותר. להעברת 1 KB של נתונים, זה מפחית את מספר החבילות מ-50 ל-3. במכשירים עם רוחב פס מוגבל (למשל, צמידי כושר), זה חוסך עד 60% אנרגיה.
לוח זמנים ותנאים
אינטגרציה בסיסית (שירות notify אחד) — 3–5 ימים. תרחישים מורכבים (OTA, ריבוי ציוד היקפי, מצב רקע) — 1–2 שבועות. העלות מחושבת באופן אישי לאחר ניתוח המכשיר שלך. צור קשר להערכת הפרויקט שלך. הזמן שילוב BLE היום — נציע אפשרויות יישום.
אנו ממליצים לקרוא את התיעוד הרשמי עבור CoreBluetooth ו-Android BluetoothGatt להבנה מפורטת יותר של ה-API. למד עוד על GATT ב-ויקיפדיה.







