Интеграция Eddystone для определения proximity в Android-приложении
Eddystone — открытый формат BLE-маяков от Google, в отличие от проприетарного iBeacon. Три основных фрейма: Eddystone-UID (16 байт идентификатора, аналог UUID/major/minor), Eddystone-URL (физический веб — маяк транслирует URL прямо в браузер без приложения), Eddystone-TLM (телеметрия: напряжение батареи, температура, счётчик пакетов). Большинство проектов используют UID для proximity и TLM для мониторинга состояния парка маяков.
Что сломается без правильной настройки
Nearby API vs прямое BLE-сканирование
Google продвигала Nearby Messages API как высокоуровневый способ работы с Eddystone. С 2023 года Nearby Messages API deprecated. Проекты, которые на него завязаны, получают предупреждение при запуске и скоро — полный отказ сервиса. Правильный путь: прямое сканирование через BluetoothLeScanner с фильтром по Service UUID 0xFEAA (Eddystone) и парсинг Advertisement Data вручную.
ScanFilter и энергопотребление
Сканирование без фильтра в SCAN_MODE_LOW_LATENCY — это полная нагрузка на BLE-чип, батарея садится за несколько часов. Правильно:
val filter = ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("0000feaa-0000-1000-8000-00805f9b34fb"))
.build()
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.build()
MATCH_MODE_AGGRESSIVE нужен, если маяки с низким txPower или через препятствия — иначе пакеты отфильтруются ещё на уровне железа. На современных устройствах с Android 12+ нужно разрешение BLUETOOTH_SCAN (не ACCESS_FINE_LOCATION для скана без определения местоположения — но это работает только при neverForLocation=true в manifest). Путаница с разрешениями убила не один релиз.
Парсинг UID-фрейма
Advertisement payload Eddystone-UID в Service Data:
Байт 0: 0x00 — тип фрейма (UID)
Байт 1: TX Power (signed int8, для калибровки расстояния)
Байты 2-11: Namespace (10 байт)
Байты 12-17: Instance (6 байт)
Смещение: Service Data начинается после AD Type 0x16 и UUID 0xAA 0xFE. Если парсить неправильно — Namespace и Instance перепутаются или сдвинутся на байт. Это не падение приложения — просто неправильный идентификатор маяка, который никогда не совпадёт с серверной базой.
Архитектура сканера
Сканирование BLE нельзя держать в Activity — она уничтожается. Используем ForegroundService с FOREGROUND_SERVICE_TYPE_LOCATION (Android 14+) или WorkManager с ExpeditedWork для коротких сессий. Результаты сканирования отправляем через BroadcastReceiver или Channel<BeaconEvent> в SharedViewModel.
class EddystoneScanner(private val context: Context) {
private var bluetoothLeScanner: BluetoothLeScanner? = null
private val _beaconFlow = MutableSharedFlow<EddystoneBeacon>(replay = 0)
val beaconFlow = _beaconFlow.asSharedFlow()
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
parseEddystoneUid(result)?.let { beacon ->
_beaconFlow.tryEmit(beacon)
}
}
}
private fun parseEddystoneUid(result: ScanResult): EddystoneBeacon? {
val serviceData = result.scanRecord
?.getServiceData(ParcelUuid.fromString("0000feaa-0000-1000-8000-00805f9b34fb"))
?: return null
if (serviceData.isEmpty() || serviceData[0] != 0x00.toByte()) return null
val txPower = serviceData[1].toInt()
val namespace = serviceData.sliceArray(2..11).toHexString()
val instance = serviceData.sliceArray(12..17).toHexString()
val rssi = result.rssi
return EddystoneBeacon(namespace, instance, rssi, txPower)
}
}
Расчёт расстояния
Расстояние по RSSI — приблизительное. Используем формулу path-loss model:
distance = 10 ^ ((txPower - rssi) / (10 * n))
где n — коэффициент среды (2.0 для открытого пространства, 3.0–4.0 для офиса с перегородками, до 4.5 для склада с металлическими стеллажами). Коэффициент определяется экспериментально на конкретном объекте — взять «стандартный» 2.0 и жаловаться на плохую точность смысла нет.
Мониторинг TLM-фреймов
Если парк маяков большой (ритейл, склад), TLM-фреймы — единственный способ понять, что батарея маяка на 3% или чип перегрелся. Парсим аналогично UID, тип фрейма 0x20. Voltage — в mV (uint16, big-endian), Temperature — 8.8 fixed-point. Результаты агрегируем на сервере через MQTT или WebSocket из сканирующего устройства.
Сроки
Базовая интеграция Eddystone-UID с расчётом proximity — 4–7 дней. С фоновым сканированием, TLM-мониторингом и серверной аналитикой — 2–4 недели. Оценка после анализа требований к точности и размеру парка маяков.







