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.







