BLE Integration: Stable Operation with Devices
We often see projects where BLE connection is implemented as "turn on scanning — connect — enjoy." In reality it's a state machine with a dozen states, each of which can fail: adapter off, device out of range, service not found, characteristic doesn't support write. Our team are specialists with 5+ years of experience developing BLE solutions for iOS and Android. We guarantee your app will stably handle all edge cases.
Why Does BLE Integration Require a Professional Approach?
BLE operates on the GATT (Generic Attribute Profile) model. The peripheral device provides Services — logical groups of functions. Each Service contains Characteristics — specific values for reading, writing, or subscribing (notify/indicate). An error at any of these levels — and the app won't receive data. We've seen projects where developers forgot to subscribe to the CCCD descriptor — as a result, the characteristic didn't send notifications.
Connection State Machine
Minimum set of states that must be explicitly handled:
IDLE → SCANNING → DISCOVERED → CONNECTING → CONNECTED → DISCOVERING_SERVICES → SERVICES_READY → SUBSCRIBING → READY And errors at each transition: scanning timeout, connection break in CONNECTING, gattStatus != GATT_SUCCESS at discovery, connection loss in READY. We test all scenarios using simulators and real devices.
| State | 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 |
What to Do If the Connection Drops?
The most common error is ignoring didDisconnectPeripheral (iOS) or onConnectionStateChange with non-GATT_SUCCESS (Android). Without automatic reconnection, the user has to restart the app. We implement logic with exponential backoff: on disconnection wait 1s, 2s, 4s, 8s, then reset. On iOS for background operation we use 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 { // handle .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 } // filter by signal 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 } // parse data according to GATT characteristic specification } } Android: BluetoothGatt
val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (status != BluetoothGatt.GATT_SUCCESS) { // status contains error code, e.g., 133 (GATT_ERROR) - needs reconnect 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 // parse } } device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) Problems We Solve in Every Project
| Problem | Solution |
|---|---|
didDisconnectPeripheral without warning | Auto-reconnect with backoff, restoration identifier (iOS) |
Status 133 GATT_ERROR on Android | gatt.close(), refresh() via reflection, retry after 2s |
| Scanning kills battery | allowDuplicates: false — enable only when measuring RSSI |
| State restoration doesn't work | Configure CBCentralManagerOptionRestoreIdentifierKey in scheme |
| Android 12+ permissions | Request BLUETOOTH_SCAN and BLUETOOTH_CONNECT dynamically |
What's Included in BLE Integration Work
- Documentation of the device's GATT scheme (services, characteristics, data formats)
- Implementation of scanning, connection, subscription, reconnection
- Handling of all normal and emergency states
- MTU optimization: request 512 bytes instead of 23 for faster transfer
- Integration with a test device (provided by the client or ours)
- Unit tests and load testing scenarios (up to 10 simultaneous BLE peripherals)
- Post-delivery support: 3-month guarantee on connection stability
How We Work: From Analysis to Deployment
- Analysis — study device documentation, agree on GATT scheme and data format.
- Design — create a state machine, define reconnection policy.
- Implementation — write code in Swift/Kotlin using modern frameworks.
- Testing — test on simulators, real devices, with interference emulation.
- Deployment — publish to TestFlight / Firebase Distribution, assist with App Review (check sections 4.2 and 5.1 of App Store Review Guidelines).
Performance and Battery
Default MTU is 23 bytes. We request via gatt.requestMtu(512) / peripheral.maximumWriteValueLength(for: .withResponse). On large transfers (firmware OTA, sensor data) this is the difference between 30 seconds and 3 minutes. Time savings using our library reach 40%. The financial efficiency of turnkey BLE integration is obvious — you get a ready solution without delving into GATT intricacies.
More about MTU
By default, BLE uses MTU of 23 bytes, of which 3 are overhead. We request 512 bytes, which is 22 times more. For transferring 1 KB of data, this reduces the number of packets from 50 to 3. On devices with limited bandwidth (e.g., fitness bracelets), this saves up to 60% energy.
Timeline and Conditions
Basic integration (one notify service) — 3–5 days. Complex scenarios (OTA, multi-peripheral, background mode) — 1–2 weeks. Cost is calculated individually after analyzing your device. Contact us for a free evaluation of your project. Order BLE integration today — we'll propose implementation options.
We recommend reading the official documentation for CoreBluetooth and Android BluetoothGatt for a more detailed understanding of the API. Learn more about GATT on Wikipedia.







