How to Develop a Companion App for Wearable IoT Devices

How to Develop a Companion App for Wearable IoT Devices? Wearable devices—activity trackers, medical patches, industrial tags—live in constant contradiction: small battery (typically 100–200 mAh), high data demand (100 Hz IMU stream, 10,000+ record buffer), and unstable connectivity. Developing a

Development and support of all types of mobile applications:

Information and entertainment mobile applications
News apps, games, reference guides, online catalogs, weather apps, fitness and health apps, travel apps, educational apps, social networks and messengers, quizzes, blogs and podcasts, forums, aggregators
E-commerce mobile applications
Online stores, B2B apps, marketplaces, online exchanges, cashback services, exchanges, dropshipping platforms, loyalty programs, food and goods delivery, payment systems.
Business process management mobile applications
CRM systems, ERP systems, project management, sales team tools, financial management, production management, logistics and delivery management, HR management, data monitoring systems
Electronic services mobile applications
Classified ads platforms, online schools, online cinemas, electronic service platforms, cashback platforms, video hosting, thematic portals, online booking and scheduling platforms, online trading platforms

These are just some of the types of mobile applications we work with, and each of them may have its own specific features and functionality, tailored to the specific needs and goals of the client.

Showing 1 of 1All 1734 services
How to Develop a Companion App for Wearable IoT Devices
Complex
from 1 week to 3 months

Our competencies:

Frequently Asked Questions

Latest works

  • image_mobile-applications_feedme_467_0.webp
    Development of a mobile application for FEEDME
    897
  • image_mobile-applications_xoomer_471_0.webp
    Development of a mobile application for XOOMER
    784
  • image_mobile-applications_rhl_428_0.webp
    Development of a mobile application for RHL
    1218
  • image_mobile-applications_zippy_411_0.webp
    Development of a mobile application for ZIPPY
    1081
  • image_mobile-applications_affhome_429_0.webp
    Development of a mobile application for Affhome
    1004
  • image_mobile-applications_flavors_409_0.webp
    Development of a mobile application for the FLAVORS company
    600

How to Develop a Companion App for Wearable IoT Devices?

Wearable devices—activity trackers, medical patches, industrial tags—live in constant contradiction: small battery (typically 100–200 mAh), high data demand (100 Hz IMU stream, 10,000+ record buffer), and unstable connectivity. Developing a companion app for such a device is first and foremost about BLE, power management, and syncing accumulated data. We specialize in custom IoT wearables where no ready-made SDK exists—only a GATT specification from the firmware team. Our experience: over 5 years and 20+ projects, from fitness trackers to medical IoT patches. Contact us for a consultation on your device.

BLE GATT Profile of a Custom Device

A custom wearable is not an Apple Watch or Fitbit. It has its own GATT service with proprietary UUIDs assigned by the manufacturer. The first task is to obtain the GATT specification from the firmware team or reverse-engineer it using nRF Connect or GATT profile (Wikipedia).

Typical GATT profile of an industrial wearable:

Service UUID Characteristic Properties Description
0x1800 Device Name Read Standard GAP
0x180F Battery Level Read, Notify Standard BAS
{custom}-0001 Raw Sensor Data Notify IMU/sensor stream
{custom}-0002 Buffered Data Read, Indicate Accumulated records
{custom}-0003 Control Point Write Device commands
{custom}-0004 Device Status Read, Notify Status, errors, uptime

Connecting and subscribing to a Notify characteristic on Android using coroutines:

class WearableRepository(private val context: Context) { private var gatt: BluetoothGatt? = null private val _sensorData = MutableSharedFlow<SensorFrame>(extraBufferCapacity = 64) val sensorData: SharedFlow<SensorFrame> = _sensorData.asSharedFlow() suspend fun connect(device: BluetoothDevice): Result<Unit> = withContext(Dispatchers.IO) { val connected = CompletableDeferred<Boolean>() gatt = device.connectGatt(context, false, object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (newState == BluetoothProfile.STATE_CONNECTED) { gatt.discoverServices() } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { connected.complete(false) scheduleReconnect(device) } } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { enableSensorNotify(gatt) connected.complete(true) } } override fun onCharacteristicChanged( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, ) { if (characteristic.uuid == SENSOR_DATA_UUID) { val frame = SensorFrame.fromBytes(value) _sensorData.tryEmit(frame) } } }, BluetoothDevice.TRANSPORT_LE) if (connected.await()) Result.success(Unit) else Result.failure(IOException("Connection failed")) } } 

BLE Stack Comparison: iOS vs Android

Parameter iOS (CoreBluetooth) Android (BluetoothGatt)
Background mode bluetooth-central background mode + restore identifier WorkManager + Service, but scanning restrictions
GATT queue Automatic serialization (single-threaded callback) Manual queue required, otherwise GATT_BUSY
Default MTU 185 bytes 23 bytes
Parallel operations Not supported Leads to disconnects
Restore session CBCentralManagerOptionRestoreIdentifierKey No built-in mechanism

How to Manage the GATT Operations Queue?

The most common source of BLE crashes is parallel GATT requests. The Android BLE stack does not support concurrent operations. You need a queue:

class GattOperationQueue { private val queue = Channel<GattOperation>(capacity = Channel.UNLIMITED) private val executor = CoroutineScope(Dispatchers.IO + SupervisorJob()) init { executor.launch { for (operation in queue) { operation.execute() // Wait for callback before next operation operation.awaitCompletion() } } } suspend fun enqueue(operation: GattOperation) { queue.send(operation) } } 

Without such a queue, a project with multiple devices simultaneously will guaranteed get onCharacteristicWrite with status=133 on some devices. Our implementation reduces such errors by 70% compared to a naive approach.

Syncing Accumulated Data

The wearable writes data to an internal buffer (flash or SRAM) when the phone is unavailable. Upon connection, the entire buffer must be read—sometimes thousands of 20-byte records. Read-out protocol via Indicate characteristic:

suspend fun syncBufferedData(): List<SensorRecord> { val records = mutableListOf<SensorRecord>() var offset = 0 do { // Request a data chunk via Control Point command writeControlPoint(ReadBufferCommand(offset = offset, count = 50)) // Wait for Indicate response val chunk = awaitIndicate(BUFFERED_DATA_UUID, timeout = 5.seconds) val parsed = SensorRecord.parseChunk(chunk) records.addAll(parsed) offset += parsed.size // Last chunk has end-of-buffer flag in header } while (!SensorRecord.isLastChunk(chunk)) // Acknowledge sync—device can clear buffer writeControlPoint(AckSyncCommand(recordsReceived = records.size)) return records } 

MTU negotiation before sync (requestMtu(512)) speeds up transfer: instead of 20 bytes per notification, you get up to 509 bytes. On iOS, default MTU is 185 bytes, negotiable to 512 on Bluetooth 5.0+. In a real project for a medical patch with a 10,000-record buffer, we cut sync time from 8 minutes to 45 seconds by increasing MTU and optimizing the read-out protocol. This is a 10x improvement—and it's free.

Why Background BLE Matters on iOS?

On iOS, all BLE work goes through CoreBluetooth. Background mode requires bluetooth-central background mode in Info.plist. Without it, Notify subscription is lost when the app goes to background—data from the device is lost.

class WearableManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { private var centralManager: CBCentralManager! private var peripheral: CBPeripheral? override init() { super.init() // CBCentralManagerOptionRestoreIdentifierKey — recovery after app termination centralManager = CBCentralManager(delegate: self, queue: DispatchQueue(label: "ble.queue"), options: [CBCentralManagerOptionRestoreIdentifierKey: "WearableSession"]) } func centralManager(_ central: CBCentralManager, willRestoreState dict: [String: Any]) { // Restore connection after OS app restart if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral], let p = peripherals.first { peripheral = p peripheral?.delegate = self } } } 

CBCentralManagerOptionRestoreIdentifierKey — without this, BLE session is lost on process restart on iOS and the device must be reconnected manually.

Over-the-Air Firmware Update (DFU)

For Nordic nRF chips — iOSDFULibrary (Swift) and Android-DFU-Library (Kotlin). For STM32WB — ST BLE Mesh DFU. We show progress with bytes and percentages, block other operations during DFU, handle interruptions—the device must be able to resume upload from the break point (DFU resume). We ensure correct connection recovery after the update.

What’s Included in the Work?

  • Analysis of the device's GATT specification and creation of interaction documentation.
  • Mobile app architecture design considering background tasks and power consumption.
  • BLE stack implementation with operation queue and error handling.
  • Buffer sync and DFU integration.
  • Testing on the real device (including oscilloscope for packet analysis).
  • 3-month post-release support.

Contact us—we will evaluate your project and propose the optimal solution. Get a consultation for your device: we'll analyze the GATT specification and determine development timelines.