Как разработать приложение-компаньон для носимых IoT-устройств?
Носимые устройства — трекеры активности, медицинские патчи, промышленные метки — живут в постоянном противоречии: батарея маленькая (типично 100–200 мА·ч), данных нужно много (поток IMU 100 Гц, буфер 10 000+ записей), связь нестабильная. Разработка приложения-компаньона для такого устройства — это прежде всего работа с BLE, управление энергопотреблением и синхронизация накопленных данных. Мы специализируемся именно на кастомных IoT-носимых, где нет готового SDK — только GATT-спецификация от firmware-команды. Наш опыт: 5+ лет и 20+ проектов, от фитнес-трекеров до медицинских IoT-патчей. Свяжитесь с нами для консультации по вашему устройству.
BLE GATT-профиль кастомного устройства
Кастомное носимое устройство — не 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 месяцев после релиза.
Свяжитесь с нами — мы оценим ваш проект и предложим оптимальное решение. Получите консультацию по вашему устройству: проанализируем GATT-спецификацию и определим сроки разработки.







