Розробка мобільного додатку-компаньйона для носимих IoT-пристроїв
Носимі пристрої — трекери активності, медичні патчі, промислові мітки — живуть у постійному протиріччі: батарея маленька (типово 100–200 мА·год), даних потрібно багато (потік IMU 100 Гц, буфер 10 000+ записів), зв'язок нестабільний. Розробка додатку-компаньйона для такого пристрою — це насамперед робота з BLE, управління енергоспоживанням та синхронізація накопичених даних. Ми спеціалізуємося саме на кастомних IoT-носимих, де немає готового SDK — лише GATT-специфікація від firmware-команди. Наш досвід: 5+ років і 20+ проєктів, від фітнес-трекерів до медичних IoT-патчів. Вартість розробки такого додатку зазвичай становить від $15,000 до $50,000 в залежності від складності. Зв'яжіться з нами для консультації щодо вашого пристрою.
BLE GATT-профіль для мобільного додатку-компаньйона кастомного IoT-пристрою
Кастомний носимий пристрій — не Apple Watch і не Fitbit. У нього свій GATT-сервіс з пропрієтарними UUID, які призначає виробник. Перше завдання — отримати специфікацію GATT від firmware-команди або реверс-інжинірити її через nRF Connect або GATT профіль (Wikipedia).
Типовий GATT-профіль промислового носимого:
| Service UUID | Characteristic | Properties | Опис |
|---|---|---|---|
0x1800 |
Device Name | Read | Стандарт GAP |
0x180F |
Battery Level | Read, Notify | Стандарт BAS |
{custom}-0001 |
Raw Sensor Data | Notify | Потік IMU/датчиків |
{custom}-0002 |
Buffered Data | Read, Indicate | Накопичені записи |
{custom}-0003 |
Control Point | Write | Команди пристрою |
{custom}-0004 |
Device Status | Read, Notify | Статус, помилки, uptime |
Підключення та підписка на Notify-характеристику на Android через корутини:
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-стеків: iOS vs Android
| Параметр | iOS (CoreBluetooth) | Android (BluetoothGatt) |
|---|---|---|
| Фоновий режим | background mode bluetooth-central + restore identifier | WorkManager + Service, але обмеження на сканування |
| Черга GATT | Automatic serialization (однопотоковий callback) | Необхідна ручна черга, інакше GATT_BUSY |
| MTU за замовчуванням | 185 байт | 23 байти |
| Parallel operations | Не підтримується | Призводить до дисконектів |
| Restore session | CBCentralManagerOptionRestoreIdentifierKey | Немає вбудованого механізму |
MTU negotiation: запит MTU до 512 (на Bluetooth 5.0+) прискорює передачу в 25 разів порівняно з дефолтним 23 байтами. На iOS MTU 185 байт за замовчуванням, negotiate до 512 на Bluetooth 5.0+. У реальному проєкті для медичного патча з буфером у 10 000 записів ми скоротили час синхронізації з 8 хвилин до 45 секунд за рахунок збільшення MTU та оптимізації протоколу вичитки. Це покращення в 10 разів швидше — і воно доступне безкоштовно.
Як керувати чергою GATT-операцій?
Найчастіше джерело крэшів при роботі з BLE — паралельні GATT-запити. Android BLE stack не підтримує конкурентні операції. Потрібна черга:
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() // Чекаємо callback перед наступною операцією operation.awaitCompletion() } } } suspend fun enqueue(operation: GattOperation) { queue.send(operation) } } Без такої черги проєкт з кількома пристроями одночасно гарантовано отримає onCharacteristicWrite з status=133 на частині пристроїв. Наша реалізація скорочує кількість таких помилок на 70% порівняно з наївним підходом.
Синхронізація даних у мобільному додатку-компаньйоні
Носимий пристрій пише дані у внутрішній буфер (flash або SRAM) коли телефон недоступний. При підключенні потрібно вичитати весь буфер — іноді кілька тисяч записів по 20 байт кожна. Протокол вичитки через Indicate-характеристику:
suspend fun syncBufferedData(): List<SensorRecord> { val records = mutableListOf<SensorRecord>() var offset = 0 do { // Запитуємо порцію даних командою в Control Point writeControlPoint(ReadBufferCommand(offset = offset, count = 50)) // Чекаємо Indicate з відповіддю val chunk = awaitIndicate(BUFFERED_DATA_UUID, timeout = 5.seconds) val parsed = SensorRecord.parseChunk(chunk) records.addAll(parsed) offset += parsed.size // Останній чанк — флаг кінця буфера в заголовку } while (!SensorRecord.isLastChunk(chunk)) // Підтверджуємо синхронізацію — пристрій може очистити буфер writeControlPoint(AckSyncCommand(recordsReceived = records.size)) return records } MTU negotiation перед синхронізацією (requestMtu(512)) прискорює передачу: замість 20 байт per notification отримуємо до 509 байт. На iOS MTU 185 байт за замовчуванням, negotiate до 512 на Bluetooth 5.0+. У реальному проєкті для медичного патча з буфером у 10 000 записів ми скоротили час синхронізації з 8 хвилин до 45 секунд за рахунок збільшення MTU та оптимізації протоколу вичитки.
Чому на iOS важлива фонова робота BLE?
На iOS вся робота з BLE через CoreBluetooth. Фоновий режим вимагає background mode bluetooth-central в Info.plist. Без нього підписка на Notify обривається коли додаток йде у фон — дані з пристрою губляться.
class WearableManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { private var centralManager: CBCentralManager! private var peripheral: CBPeripheral? override init() { super.init() // CBCentralManagerOptionRestoreIdentifierKey — відновлення після kill додатку centralManager = CBCentralManager(delegate: self, queue: DispatchQueue(label: "ble.queue"), options: [CBCentralManagerOptionRestoreIdentifierKey: "WearableSession"]) } func centralManager(_ central: CBCentralManager, willRestoreState dict: [String: Any]) { // Відновлюємо підключення після перезапуску додатку ОС if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral], let p = peripherals.first { peripheral = p peripheral?.delegate = self } } } CBCentralManagerOptionRestoreIdentifierKey — без цього на iOS при перезапуску процесу губиться сесія BLE і пристрій доводиться перепідключати вручну.
Оновлення прошивки по повітрю (DFU)
Для Nordic nRF-чипів — бібліотека iOSDFULibrary (Swift) і Android-DFU-Library (Kotlin). Для STM32WB — ST BLE Mesh DFU. Показуємо прогрес з байтами та відсотками, блокуємо інші операції на час DFU, обробляємо переривання — пристрій повинен вміти відновити завантаження з місця переривання (DFU resume). Гарантуємо коректне відновлення з'єднання після оновлення.
Що входить в роботу?
- Аналіз GATT-специфікації пристрою та формування документації щодо взаємодії.
- Проектування архітектури мобільного додатку з урахуванням фонових завдань та енергоспоживання.
- Реалізація BLE-стека з чергою операцій та обробкою помилок.
- Інтеграція синхронізації буфера та DFU.
- Тестування на реальному пристрої (в тому числі з осцилографом для аналізу пакетів).
- Супровід протягом 3 місяців після релізу.
Приклад з практики: для одного з наших клієнтів — виробника медичних патчів — ми розробили додаток, який синхронізує дані з 32 пристроїв одночасно, що дозволило лікарям отримувати дані в реальному часі.
Зв'яжіться з нами — ми оцінимо ваш проєкт і запропонуємо оптимальне рішення. Отримайте консультацію щодо вашого пристрою: проаналізуємо GATT-специфікацію та визначимо терміни розробки.







