Розробка мобільного додатку-компаньйона для носимих 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-специфікацію та визначимо терміни розробки.







