BLE-інтеграція: стабільна робота з пристроями
Ми часто бачимо проекти, де BLE-підключення реалізоване як «увімкнув сканування — підключився — радіємо». У реальності це кінцевий автомат з дюжиною станів, кожен з яких може завершитися помилкою: адаптер вимкнено, пристрій поза зоною, сервіс не знайдено, характеристика не підтримує запис. Наша команда — спеціалісти з 5+ річним досвідом розробки BLE-рішень для iOS та Android. Ми гарантуємо, що ваше застосування стабільно обробляє всі граничні випадки. Наша реалізація зменшує кількість обривів з'єднання в 5 разів порівняно з непрофесійною інтеграцією.
Чому BLE-інтеграція потребує професійного підходу?
BLE працює за моделлю GATT (Generic Attribute Profile). Периферійний пристрій надає Services — логічні групи функцій. Кожен Service містить Characteristics — конкретні значення для читання, запису або підписки (notify/indicate). Помилка на будь-якому з цих рівнів — і застосування не отримає дані. Ми бачили проекти, де розробники забували підписуватися на дескриптор CCCD — в результаті характеристика не надсилала сповіщення.
Кінцевий автомат підключення
Мінімальний набір станів, які потрібно обробляти явно:
IDLE → SCANNING → DISCOVERED → CONNECTING → CONNECTED → DISCOVERING_SERVICES
→ SERVICES_READY → SUBSCRIBING → READY
І помилки на кожному переході: таймаут сканування, розрив з'єднання в CONNECTING, gattStatus != GATT_SUCCESS при discovery, втрата з'єднання в READY. Тестуємо всі сценарії за допомогою симуляторів та реальних пристроїв.
| Стан | iOS (CoreBluetooth) | Android (BluetoothGatt) |
|---|---|---|
| IDLE | .poweredOn |
BluetoothAdapter.state_on |
| SCANNING | scanForPeripherals |
startScan |
| DISCOVERED | didDiscover |
onScanResult |
| CONNECTING | connect |
connectGatt |
| CONNECTED | didConnect |
onConnectionStateChange (STATE_CONNECTED) |
| DISCOVERING_SERVICES | discoverServices |
discoverServices |
| SERVICES_READY | didDiscoverServices |
onServicesDiscovered |
| SUBSCRIBING | setNotifyValue |
writeDescriptor (CCCD) |
| READY | didUpdateValue |
onCharacteristicChanged |
Що робити, якщо з'єднання розірвалося?
Найчастіша помилка — ігнорування didDisconnectPeripheral (iOS) або onConnectionStateChange з не-GATT_SUCCESS (Android). Без автоматичного реконекту користувач змушений перезапускати застосування. Ми реалізуємо логіку з експоненційним backoff: при розриві чекаємо 1 с, 2 с, 4 с, 8 с, потім скидаємо. На iOS для роботи у фоні використовуємо CBCentralManagerOptionRestoreIdentifierKey — system wakes up the app when device reappears.
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-спецификации характеристики
}
}
Android: 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)
Проблеми, які ми вирішуємо в кожному проекті
| Проблема | Рішення |
|---|---|
didDisconnectPeripheral без попередження |
Автореконект з backoff, restoration identifier (iOS) |
Статус 133 GATT_ERROR на Android |
gatt.close(), refresh() через рефлексію, повтор через 2 с |
| Сканування вбиває батарею | allowDuplicates: false — вмикаємо тільки при вимірюванні RSSI |
| State restoration не працює | Налаштовуємо CBCentralManagerOptionRestoreIdentifierKey у схемі |
| Android 12+ дозволи | Запитуємо BLUETOOTH_SCAN та BLUETOOTH_CONNECT динамічно |
Що входить в роботу з інтеграції BLE
- Документація по GATT-схемі пристрою (сервіси, характеристики, формати даних)
- Реалізація сканування, підключення, підписки, реконекту
- Обробка всіх штатних та аварійних станів
- Оптимізація MTU: запит 512 байт замість 23 для прискорення передачі
- Інтеграція з тестовим пристроєм (наданим замовником або нашим)
- Unit-тести та сценарії навантажувального тестування (до 10 одночасних BLE-периферій)
- Підтримка після здачі: гарантія 3 місяці на стабільність з'єднання
- Ціна: від 500$ за базовий модуль
Як ми працюємо: від аналізу до деплою
- Аналітика — вивчаємо документацію пристрою, узгоджуємо GATT-схему та формат даних.
- Проектування — створюємо кінцевий автомат, визначаємо політику реконекту.
- Реалізація — пишемо код на Swift/Kotlin з використанням сучасних фреймворків.
- Тестування — перевіряємо на симуляторах, реальних пристроях, з емуляцією перешкод.
- Деплой — публікуємо в TestFlight / Firebase Distribution, допомагаємо з App Review (перевірка розділу 4.2 та 5.1 App Store Review Guidelines).
Продуктивність і батарея
MTU за замовчуванням — 23 байти. Запитуємо через gatt.requestMtu(512) / peripheral.maximumWriteValueLength(for: .withResponse). Оптимізація MTU прискорює передачу даних до 22 разів порівняно зі стандартним розміром пакета. На одному з проектів для фітнес-трекера ми скоротили час передачі даних з 8 с до 1.2 с завдяки оптимізації MTU та реконекту. Економія енергії до 60% на пристроях з обмеженим каналом. Економічна ефективність інтеграції BLE під ключ очевидна — ви отримуєте готове рішення без необхідності вникати в тонкощі GATT.
Детальніше про MTU
За замовчуванням BLE використовує MTU 23 байти, з яких 3 — службові. Ми запитуємо 512 байт, що в 22 рази більше. Для передачі даних об'ємом 1 Кбайт це скорочує кількість пакетів з 50 до 3. На пристроях з обмеженим каналом (наприклад, фітнес-браслети) це економить до 60% енергії.
Терміни та умови
Базова інтеграція (один notify-сервіс) — 3-5 днів, від 500$. Складні сценарії (OTA, мультипериферія, background mode) — 1-2 тижні. Вартість базової інтеграції — від 500$. Зв'яжіться з нами для безкоштовної оцінки вашого проекту. Замовте інтеграцію BLE сьогодні — ми запропонуємо варіанти реалізації.
Рекомендуємо прочитати офіційну документацію CoreBluetooth та Android BluetoothGatt для більш детального розуміння API. Докладніше про GATT читайте в Wikipedia.







