Использование технологии iBeacon в React Native

Время прочтения — 14 минут
Содержание
В предыдущей статье мы рассказали про использование беакон-маяков и технологии BLE в приложениях на React Native c помощью библиотеки React Native BLE. Теперь перейдем к более совершенному подходу для работы с маяками, — разработаем нативный модуль на iOS.
Чем же он лучше? Можно добиться большей точности, обеспечить глубокую интеграцию с iOS и лучшее управление энергопотреблением.
В этой статье мы сравним оба подхода и обсудим, чем же нативное решение лучше для определенных типов приложений. Еще подробно остановимся на технической реализации, настройке маяков и расскажем, зачем они нужны для бизнеса.

В чем разница между BLE и iBeacon?

Bluetooth Low Energy (BLE) и iBeacon работают на одной и той же технологической основе, но предназначены для разных целей и имеют различные области применения.

BLE (Bluetooth Low Energy)

BLE разработан для передачи небольших объемов данных на короткие расстояния с минимальным энергопотреблением. Это идеально подходит для устройств, работающих от батарей, таких как фитнес-браслеты, датчики окружающей среды, умные часы и другие устройства Интернета вещей (IoT).
Основное преимущество BLE — это его способность работать в течение длительного времени на одном заряде батареи благодаря низкому энергопотреблению.
Благодаря BLE можно создавать кучу разных приложений, в которых требуется передача данные между устройствами на коротких расстояниях.

iBeacon

iBeacon — это протокол, разработанный Apple, который использует технологию BLE для предоставления геолокационных услуг в конкретных сценариях. Он позволяет мобильным приложениям определять их близость к маячку, небольшому устройству BLE, которое транслирует уникальный идентификатор.
В отличие от BLE, который отвечает за простой обмен данными между устройствами, iBeacon используют для создания более сложных сценариев взаимодействия. Например, приложение должно реагировать, когда пользователь приближается к определенному месту или покидает его.

Преимущества iBeacon

Протокол iBeacon особенно подходит для приложений, требующих точного и контекстно-зависимого взаимодействия с пользователем внутри помещений.
Благодаря упрощенной настройке геозон, лучшей поддержке фонового режима и повышенной точности определения местоположения. Это делает iBeacon идеальным для целей маркетинга в розничной торговле, внутренней навигации и автоматизации в помещениях.

Упрощенная настройка геозон

iBeacon позволяет легко создавать геозоны вокруг физических маяков. Разработчикам не нужно заботиться о сложной настройке и обслуживании сети GPS внутри помещений. Поскольку не гарантирует такой точности и эффективности. В то же время, iBeacon позволяет легко определять, когда пользователь входит или выходит из заданной геозоны.

Лучшая поддержка фонового режима

iOS позволяет приложению работать в фоновом режиме. Оно получит уведомление о пересечении границы геозоны даже, когда не используется. Благодаря этому можно, например, автоматически предлагать купоны или специальные предложения, когда человек приближается к магазину. Или же трекать важные события для логистических и мониторинговых приложений без активного участия пользователя.

Повышенная точность определения местоположения внутри помещений

В условиях ограниченного пространства, таких как торговые центры, музеи, конференц-залы и другие внутренние локации, GPS ненадежен из-за слабого сигнала и отсутствия точности. iBeacon гораздо лучше определяет местоположения пользователей. Это открывает двери для разработки более точных систем внутренней навигации, позволяя, например, направлять посетителей к определенным экспонатам в музее или предлагать товары, находящиеся в непосредственной близости к пользователю в магазине.

Беакон-маячок

Я использовала маячок Holyiot nRF52810. Он выделяется своим ультранизким энергопотреблением, компактным размером и водонепроницаемым корпусом.
С помощью приложения Holyiot-beacon можно легко настроить маячок становятся и адаптировать параметры под конкретные нужды.

Общие характеристики

• Мощность сигнала и дальность действия:
Маячок способен передавать сигнал на расстояние до 50 метров в открытом пространстве с регулируемой мощностью передачи от -40dB до +4dB. По умолчанию мощность установлена на +4dB, что оптимизирует дальность и энергопотребление.
• Длительность жизни батареи:
С учетом емкости батареи CR2032 в 220 mAh и среднего энергопотребления в 49uA при +4dB и интервале передачи в 500ms, маячок обеспечивает работу до 180 дней без замены батареи. Это делает маячок особенно подходящим для задач, требующих долгосрочного размещения без обслуживания.
• Водонепроницаемость:
Класс защиты IP67 делает маячок стойким к воздействию воды и пыли, что расширяет возможности его использования в различных условиях.
• Физические параметры:
Маячок имеет компактные размеры с диаметром 30 мм и толщиной 8.4 мм при весе всего 6.5 г, что делает его легко интегрируемым в любую среду.

Настройка маячка

1
Загрузка приложения
• Откройте магазин приложений на смартфоне (App Store для iOS или Google Play для Android).
• В поиске введите «Holyiot-beacon» и найдите приложение для конфигурации маяков от Holyiot.
• Установите приложение на устройство.
2
Поиск устройства
• Запустите приложение «Holyiot-beacon» на смартфоне.
• В приложении выберите тип устройства для поиска: может быть выбран beacon, ibeacon или eddystone в зависимости от потребностей.
• Приложение начнет поиск доступных устройств поблизости.
3
Подключение к маяку
• Когда маяк будет обнаружен, выберите его в списке доступных устройств.
• Для подключения к маяку и доступа к дополнительной информации нажмите на кнопку «Connect».
• При запросе введите пароль для подключения. По умолчанию пароль: х`aa14061112`.
4
Конфигурация маяка
Конфигурация параметров позволяет точно настроить работу маяка под конкретные требования приложения или сервиса, обеспечивая нужный баланс между видимостью, энергопотреблением и точностью позиционирования.
После успешного подключения вы сможете просмотреть детальную информацию о маяке и доступные для настройки параметры. В разделе настроек выберите параметры, которые хотите изменить.
Конфигурация маяка включает в себя настройку различных параметров, каждый из которых играет важную роль в функционировании маяка и его взаимодействии с приложениями и устройствами. Вот основные параметры, которые обычно настраиваются при конфигурации маяка:
UUID (Универсальный уникальный идентификатор)
UUID — это 128-битный идентификатор, который используется для обозначения определенной группы или категории маяков в большом пространстве. Он позволяет мобильному приложению отличать маяки вашей организации.
Major и Minor
Major и Minor — это числовые значения, используемые для идентификации подгруппы внутри группы маяков с одинаковым UUID. Major обычно определяет более крупную подгруппу, а Minor — более мелкую. Эти параметры позволяют создавать дополнительную иерархию или структурирование внутри вашей системы маяков, упрощая управление и целенаправленное взаимодействие в приложении.
TX Power (Мощность передачи)
TX Power отражает мощность сигнала, с которой маяк передает свои данные. Этот параметр напрямую влияет на дальность действия маяка и потребление энергии. Настройка оптимальной мощности передачи позволяет сбалансировать видимость маяка и длительность его работы от батареи.
ADV_interval (Интервал рекламы)
ADV_interval — это время между последовательными передачами рекламных пакетов Bluetooth от маяка. Интервал влияет на то, насколько часто маяк «объявляет» о себе, что влияет на энергопотребление и скорость реакции приложений на появление или исчезновение маяка из зоны действия. Уменьшение интервала увеличивает шансы на быстрое обнаружение маяка, но также увеличивает энергопотребление.
Внесите необходимые изменения и сохраните настройки. Некоторые изменения могут потребовать повторного подключения к маяку или перезагрузки маяка.

Реализация нативного модуля в React Native для iOS

Когда мы разрабатывали приложение на React Native, искали решение для работы с маяками iBeacon, но не нашли такого, которое бы полностью соответствовало нашим требованиям. Поэтому решили разработать собственный нативный модуль для iOS, чтобы максимально использовать возможности этой платформы для взаимодействия с маяками.

Создание нативного модуля

Сперва создаем нативный модуль для iOS, который интегрируем с React Native. Для этого пишем код на Swift или Objective-C, который будет вызван из JavaScript. Этот процесс включает в себя объявление методов, доступных для вызова из React Native, и настройку событийной модели для обмена данными между нативной и JavaScript частями приложения.
Подробные инструкции по созданию и интеграции нативных модулей в проект React Native, включая примеры кода и наилучшие практики, вы найдете в официальной документации React Native.

Настройка необходимых разрешений

После создания нативного модуля следующий этап — настройка необходимых разрешений и возможностей в вашем проекте Xcode, чтобы обеспечить его корректную работу с маяками и уведомлениями.
Откройте настройки проекта в Xcode и перейдите в раздел «Signing & Capabilities".
Здесь необходимо добавить два ключевых capability: «Background Modes" и "Push Notifications".
• Активация «Location updates» и «Uses Bluetooth LE accessories» в разделе «Background Modes» позволяет приложению получать данные о местоположении и работать с Bluetooth-аксессуарами, даже когда оно находится в фоновом режиме.
• Включение «Push Notifications» необходимо, чтобы приложение могло отправлять пользователю локальные уведомления при входе или выходе из зоны действия маяка.
Для корректной работы с маяками и Bluetooth в iOS, важно добавить в файл Info.plist проекта необходимые строки разрешений. Эти строки информируют пользователя о том, как приложение собирается использовать данные или функции устройства.
Вот ключевые разрешения, которые обычно требуются:
1
Privacy - Location Always and When In Use Usage Description
(NSLocationAlwaysAndWhenInUseUsageDescription): Требуется описание, почему приложению нужен доступ к данным о местоположении пользователя в любое время, включая фоновый режим.
Например, «Это приложение использует ваше местоположение для определения близости к интересующим вас маякам, даже когда приложение находится в фоновом режиме».
2
Privacy - Location When In Use Usage Description
(`NSLocationWhenInUseUsageDescription`): Необходимо указать причину, по которой приложению требуется доступ к информации о местоположении, когда оно используется.
Пример описания: «Приложению требуется доступ к вашему местоположению, чтобы уведомлять вас о маяках поблизости, когда вы используете приложение».
3
Privacy - Bluetooth Always Usage Description
(`NSBluetoothAlwaysUsageDescription`): Поскольку модуль использует Bluetooth для определения маяков, необходимо объяснить, зачем приложению постоянный доступ к Bluetooth.
Пример: «Доступ к Bluetooth используется для обнаружения маяков вокруг вас, чтобы предоставить соответствующую информацию и уведомления».
4
Privacy - Bluetooth Peripheral Usage Description
(`NSBluetoothPeripheralUsageDescription`): Это объяснение нужно, если ваше приложение собирается выступать в роли Bluetooth-периферии.
Например: «Это приложение использует Bluetooth для связи с маяками и другими устройствами в вашей окрестности».

Реализация

BeaconManager.swift

import CoreBluetooth
import CoreLocation
import React
import UserNotifications

// Объявляем класс BeaconManager, который реализует интерфейсы для работы с React Native и делегаты для мониторинга маяков и Bluetooth
@objc(BeaconManager)
class BeaconManager: NSObject, RCTBridgeModule, CLLocationManagerDelegate, CBCentralManagerDelegate
{

  // Имя модуля для React Native
  static func moduleName() -> String {
    return "BeaconManager"
  }

  private var locationManager: CLLocationManager!
  private var beaconRegion: CLBeaconRegion!
  public var bridge: RCTBridge!
  private var centralManager: CBCentralManager!

  // Метод для отправки локальных уведомлений
  func sendLocalNotification(with message: String) {
    let content = UNMutableNotificationContent()
    content.title = message  // Заголовок уведомления
    content.body = "This is a region event"  // Текст уведомления
    content.sound = .default  // Звук уведомления

    let request = UNNotificationRequest(
      identifier: UUID().uuidString, content: content, trigger: nil)  // Создание запроса на уведомление
    UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)  // Добавление запроса в центр уведомлений
  }

  // Начать сканирование маяков с заданным UUID
  @objc func startScanning(_ uuid: String, config: NSDictionary) {
    // Запрос разрешений на отправку уведомлений
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) {
      granted, error in
      if granted {
        print("Notifications allowed")
      } else {
        print("Notifications not allowed")
      }
    }
    DispatchQueue.main.async {
      self.locationManager = CLLocationManager()  // Инициализация CLLocationManager
      self.locationManager.delegate = self  // Установка делегата
      self.locationManager.requestAlwaysAuthorization()  // Запрос на постоянный доступ к геолокации

      // Проверка и установка настроек для сканирования в фоновом режиме
      self.locationManager.allowsBackgroundLocationUpdates = true
      self.locationManager.pausesLocationUpdatesAutomatically = false

      let uuid = UUID(uuidString: uuid)!  // Преобразование строки UUID в UUID
      let beaconConstraint = CLBeaconIdentityConstraint(uuid: uuid)  // Создание ограничения для маяка
      self.beaconRegion = CLBeaconRegion(
        beaconIdentityConstraint: beaconConstraint, identifier: "BeaconManagerRegion")  // Инициализация региона маяка
      self.beaconRegion.notifyOnEntry = true  // Уведомление при входе в регион
      self.beaconRegion.notifyOnExit = true  // Уведомление при выходе из региона

      self.locationManager.startMonitoring(for: self.beaconRegion)  // Начало мониторинга региона
      self.locationManager.startRangingBeacons(in: self.beaconRegion)  // Начало определения расстояния до маяков в регионе
    }
  }

  // Остановить сканирование маяков
  @objc func stopScanning() {
    if let beaconRegion = self.beaconRegion {
      self.locationManager.stopMonitoring(for: beaconRegion)  // Остановка мониторинга региона
      self.locationManager.stopRangingBeacons(in: beaconRegion)  // Остановка определения расстояния до маяков
      self.beaconRegion = nil  // Сброс региона маяка
      self.locationManager = nil  // Сброс CLLocationManager
    }
  }

  // Инициализация менеджера Bluetooth
  @objc func initializeBluetoothManager() {
    centralManager = CBCentralManager(
      delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: false])
  }

  // Обработка изменения состояния Bluetooth
  func centralManagerDidUpdateState(_ central: CBCentralManager) {
    var msg = ""
    switch central.state {
    case .unknown: msg = "unknown"
    case .resetting: msg = "resetting"
    case .unsupported: msg = "unsupported"
    case .unauthorized: msg = "unauthorized"
    case .poweredOff: msg = "poweredOff"
    case .poweredOn: msg = "poweredOn"
    @unknown default: msg = "unknown"
    }
    bridge.eventDispatcher().sendAppEvent(withName: "onBluetoothStateChanged", body: ["state": msg])  // Отправка события изменения состояния Bluetooth в React Native
  }

  // Запрос на постоянный доступ к геолокации
  @objc func requestAlwaysAuthorization(
    _ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    let locationManager = CLLocationManager()
    locationManager.delegate = self
    locationManager.requestAlwaysAuthorization()
    let status = CLLocationManager.authorizationStatus()
    let statusString = statusToString(status)
    resolve(["status": statusString])
  }

  // Запрос на доступ к геолокации при использовании приложения
  @objc func requestWhenInUseAuthorization(
    _ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    let locationManager = CLLocationManager()
    locationManager.delegate = self
    locationManager.requestWhenInUseAuthorization()
    let status = CLLocationManager.authorizationStatus()
    let statusString = statusToString(status)
    resolve(["status": statusString])
  }

  // Получение текущего статуса разрешений геолокации
  @objc func getAuthorizationStatus(
    _ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    let status = CLLocationManager.authorizationStatus()
    resolve(statusToString(status))
  }

  // Обработка событий входа в регион и выхода из региона
  func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
    if let beaconRegion = region as? CLBeaconRegion {
      sendLocalNotification(with: "Entered region: \(region.identifier)")  // Отправка уведомления о входе в регион
      if let bridge = self.bridge {
        bridge.eventDispatcher().sendAppEvent(
          withName: "onEnterRegion", body: ["region": beaconRegion.identifier])  // Отправка события входа в регион в React Native
      }
    }
  }

  func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
    if let beaconRegion = region as? CLBeaconRegion {
      sendLocalNotification(with: "Exit region: \(region.identifier)")  // Отправка уведомления о выходе из региона
      if let bridge = self.bridge {
        bridge.eventDispatcher().sendAppEvent(
          withName: "onExitRegion", body: ["region": beaconRegion.identifier])  // Отправка события выхода из региона в React Native
      }
    }
  }

  // Обработка обнаружения маяков в регионе
  func locationManager(
    _ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion
  ) {
    let beaconArray = beacons.map { beacon -> [String: Any] in
      return [
        "uuid": beacon.uuid.uuidString,  // UUID маяка
        "major": beacon.major.intValue,  // Major значение маяка
        "minor": beacon.minor.intValue,  // Minor значение маяка
        "distance": beacon.accuracy,  // Точность расстояния до маяка
        "rssi": beacon.rssi,  // Мощность сигнала маяка
      ]
    }
    if let bridge = bridge {
      bridge.eventDispatcher().sendAppEvent(withName: "onBeaconsDetected", body: beaconArray)  // Отправка данных об обнаруженных маяках в React Native
    }
  }

  // Обработка изменения разрешений геолокации
  func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
    if #available(iOS 14.0, *) {
      if manager.authorizationStatus == .authorizedAlways
        || manager.authorizationStatus == .authorizedWhenInUse
      {
        locationManager.startMonitoring(for: beaconRegion)  // Начало мониторинга региона
        locationManager.startRangingBeacons(in: beaconRegion)  // Начало определения расстояния до маяков
      }
    } else {
      if CLLocationManager.authorizationStatus() == .authorizedAlways
        || CLLocationManager.authorizationStatus() == .authorizedWhenInUse
      {
        locationManager.startMonitoring(for: beaconRegion)
        locationManager.startRangingBeacons(in: beaconRegion)
      }
    }
  }

  // Вспомогательный метод для преобразования статуса разрешения геолокации в строку
  private func statusToString(_ status: CLAuthorizationStatus) -> String {
    switch status {
    case .notDetermined: return "notDetermined"
    case .restricted: return "restricted"
    case .denied: return "denied"
    case .authorizedAlways: return "authorizedAlways"
    case .authorizedWhenInUse: return "authorizedWhenInUse"
    @unknown default: return "unknown"
    }
  }
}
BeaconManagerBridge.m

#import "React/RCTBridgeModule.h"

@interface RCT_EXTERN_MODULE(BeaconManager, NSObject)

RCT_EXTERN_METHOD(startScanning:(NSString *)uuid config:(NSDictionary *)config)
RCT_EXTERN_METHOD(stopScanning)
RCT_EXTERN_METHOD(requestAlwaysAuthorization:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(requestWhenInUseAuthorization:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(getAuthorizationStatus:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(initializeBluetoothManager)

+ (BOOL)requiresMainQueueSetup {
  return YES;
}

@end
App.js

import React, {useEffect, useState} from 'react';
import {View, NativeModules, Text} from 'react-native';
import {DeviceEventEmitter} from 'react-native';

const {BeaconManager} = NativeModules;

BeaconManager.requestAlwaysAuthorization();

BeaconManager.startScanning('FDA50693-A4E2-4FB1-AFCF-C6EB07647825');

const App = () => {
  const [inRegion, setInRegion] = useState(false);
  useEffect(() => {
    DeviceEventEmitter.addListener('onBeaconsDetected', beacons => {
      console.log('onBeaconsDetected', beacons);
    });
  }, []);

  useEffect(() => {
    DeviceEventEmitter.addListener('onEnterRegion', beacons => {
      setInRegion(true);
    });
  }, []);

  useEffect(() => {
    DeviceEventEmitter.addListener('onExitRegion', beacons => {
      setInRegion(false);
    });
  }, []);

  return (
    <View
      style={{
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        paddingHorizontal: 32,
        backgroundColor: inRegion ? '#62BB46' : '#472F92',
      }}>
      <Text style={{color: '#fff', fontWeight: 600, fontSize: 16}}>
        {inRegion
          ? 'You are within the range of the beacon'
          : 'You are out of range of the beacon'}
      </Text>
    </View>
  );
};

export default App;

Работа с маяками в фоновом режиме

Методы `didEnterRegion` и `didExitRegion` являются частью протокола `CLLocationManagerDelegate` и вызываются, когда устройство входит в зону действия маяка или выходит из неё соответственно. Эти методы позволяют приложениям реагировать на изменения местоположения пользователя даже в фоновом режиме.

Ограничения и особенности

Ограничения iOS на фоновую работу

Несмотря на то, что iOS позволяет приложениям мониторить маяки в фоновом режиме, существуют определенные ограничения, связанные с сохранением заряда батареи и производительностью устройства.
Apple может ограничивать частоту обновлений местоположения или временно приостанавливать фоновую активность приложения для оптимизации работы системы. Также iOS предоставляет ограниченное время для выполнения необходимых действий в ответ на это событие. Это время может варьироваться, но обычно составляет несколько секунд. За это время приложение должно успеть выполнить все необходимые операции.

Ограничения на отправку уведомлений

Хотя методы позволяют реагировать на вход и выход из регионов в фоне, отправка уведомлений пользователю также подчиняется строгим правилам iOS. Приложение должно получить соответствующее разрешение от пользователя на отправку уведомлений.

Задержки реагирования

В зависимости от настроек системы и условий окружающей среды, могут наблюдаться задержки между фактическим входом или выходом из региона и моментом вызова соответствующих методов. Это обусловлено как особенностями работы Bluetooth, так и алгоритмами оптимизации энергопотребления iOS.

Требования к точности и конфигурации маяка

Для оптимальной работы в фоновом режиме, важно обеспечить правильную конфигурацию маяков и настроить приложение на работу с необходимой точностью определения местоположения.
Работа с маяками в фоне значительно расширяет функциональные возможности приложений, позволяя создавать продвинутые решения для локационных сервисов, навигации внутри зданий, автоматизированного взаимодействия с пользователем и многого другого. Однако при разработке таких приложений важно учитывать упомянутые выше особенности и ограничения, чтобы обеспечить наилучшую производительность и пользовательский опыт.

Если вас заинтересовала эта тема, и вы хотите разобраться, как можно применять маяки в жизни, читайте нашу статью "Технология BLE для бизнеса"