首页 文章

即使设备在信标附近保持静止,应用程序也会在didEnterRegion()和didExitRegion()之间循环

提问于
浏览
1

我正在使用AltBeacon Android Library(我转载v2.9.2的问题;以及v2.11)与Onyx和kontact.io提供的iBeacon设备集成 .

该库似乎运行良好,但我似乎有一个问题,我无法找到一个可接受的解决方案 .

以下是有关我如何使用AltBeacon库以及该问题的更多详细信息:

  • 设备在信标附近静止不动

  • 蓝牙开启

  • 应用程序在前台运行

  • BeaconManager配置为使用以下设置在前台模式下扫描:

BeaconManager.setRegionExitPeriod(30000L);
beaconManager.setBackgroundBetweenScanPeriod(120000L);
beaconManager.setForegroundScanPeriod(5000L);
beaconManager.setForegroundBetweenScanPeriod(10000L);
beaconManager.getBeaconParsers().add(
new BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24"));
  • Application将BeaconManager设置为前台模式
beaconManager.setBackgroundMode(false);
  • 应用程序绑定到BeaconManager
beaconManager.bind(…)
  • 当触发 onBeaconServiceConnect() 时,应用程序开始监视特定区域中的信标(我要监视的信标列表是已知的,静态的;我使用区域列表,我想要监视的每个信标的一个不同区域)
beaconManager.startMonitoringBeaconsInRegion(region);
  • 当设备进入信标区域( didEnterRegion() 被调用)时,应用程序开始输入区域的范围
beaconManager.startRangingBeaconsInRegion(region);
  • 检测到信标(为相应的信标调用 didRangeBeaconsInRegion()

  • 应用程序切换信标扫描到后台模式:

beaconManager.setBackgroundMode(true);
  • 几分钟后,即使没有移动设备和信标并且应用程序保持相同状态,也会调用 didExitRegion() .

我发现了两个描述相同问题的Stackoverflow问题:

我目前使用的解决方法是Stackoverflow问题中建议的解决方法:

  • 我已将beacon Advertising Frequency 值从1000毫秒更新为 100 ms .

一旦频率增加,一切似乎都能正常工作,但解决方案是不可接受的,因为信标的电池寿命是 drastically impaired .

所有信标扫描都在后台执行(即没有使用Activity):

import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import org.altbeacon.beacon.Beacon;
import org.altbeacon.beacon.BeaconConsumer;
import org.altbeacon.beacon.BeaconManager;
import org.altbeacon.beacon.BeaconParser;
import org.altbeacon.beacon.Identifier;
import org.altbeacon.beacon.MonitorNotifier;
import org.altbeacon.beacon.RangeNotifier;
import org.altbeacon.beacon.Region;
import org.altbeacon.beacon.powersave.BackgroundPowerSaver;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class BeaconDataProvider implements BeaconConsumer, RangeNotifier, MonitorNotifier {

  private final Logger LOGGER = LogFactory.get(this);
  private final Context applicationContext;
  private final BeaconIdentifierFactory beaconIdentifierFactory;
  private final BeaconScanningListener beaconScanningListener;

  private BeaconManager beaconManager;
  private Collection<Region> targetedRegions;

  /**
   * This field is used for improving battery consumption. Do not remove it.
   */
  @SuppressWarnings({"unused", "FieldCanBeLocal"})
  private BackgroundPowerSaver backgroundPowerSaver;

  public BeaconDataProvider(Context applicationContext, BeaconIdentifierFactory beaconIdentifierFactory,
      BeaconScanningListener beaconScanningListener) {
    LOGGER.v("BeaconDataProvider - new instance created.");
    this.applicationContext = applicationContext;
    this.beaconIdentifierFactory = beaconIdentifierFactory;
    this.beaconScanningListener = beaconScanningListener;
    beaconManager = BeaconManager.getInstanceForApplication(applicationContext);
    LOGGER.v("BeaconManager hashCode=%s", beaconManager.hashCode());
    BeaconManager.setRegionExitPeriod(30000L);
    beaconManager.setBackgroundBetweenScanPeriod(120000L);
    beaconManager.setForegroundScanPeriod(5000L);
    beaconManager.setForegroundBetweenScanPeriod(10000L);
    beaconManager.getBeaconParsers().add(
        new BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24"));
    backgroundPowerSaver = new BackgroundPowerSaver(applicationContext);
  }

  public void setBackgroundMode() {
    LOGGER.i("setBackgroundMode()");
    beaconManager.setBackgroundMode(true);
  }

  public void setForegroundMode() {
    LOGGER.i("setForegroundMode()");
    beaconManager.setBackgroundMode(false);
  }

  public boolean checkAvailability() {
    return android.os.Build.VERSION.SDK_INT >= 18 && applicationContext.getPackageManager()
        .hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);

  }

  public boolean isBluetoothEnabled() {
    BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    boolean result = mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
    LOGGER.i("isBluetoothEnabled() -> %s", result);
    return result;
  }

  public boolean isLocationPermissionGranted(Context context) {
    return (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
        && context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
        == PackageManager.PERMISSION_GRANTED);
  }

  public void startScanning(Collection<BeaconIdentifier> targetedBeacons) {
    LOGGER.i("startScanning()");
    if (!beaconManager.isBound(this)) {
      this.targetedRegions = getRegionsForTargetedBeacons(targetedBeacons);
      beaconManager.bind(this);
    }
    else {
      LOGGER.i("Scanning already started.");
    }
  }

  @NonNull
  private List<Region> getRegionsForTargetedBeacons(Collection<BeaconIdentifier> beaconIdentifiers) {
    List<Region> regions = new ArrayList<>();
    for (BeaconIdentifier beaconIdentifier : beaconIdentifiers) {
      try {
        Region region = new Region(beaconIdentifier.getRegionId(), Identifier.parse(beaconIdentifier.getUuid()),
            Identifier.parse(String.valueOf(beaconIdentifier.getMajor())),
            Identifier.parse(String.valueOf(beaconIdentifier.getMinor())));
        regions.add(region);
      }
      catch (Exception e) {
        LOGGER.e("Caught exception.", e);
        LOGGER.w("Failed to create region for beaconIdentifier=%s", beaconIdentifier.getCallParamRepresentation());
      }
    }
    return regions;
  }

  public void stopScanning() {
    LOGGER.i("stopScanning()");
    if (beaconManager.isBound(this)) {
      for (Region region : targetedRegions) {
        try {
          beaconManager.stopMonitoringBeaconsInRegion(region);
        }
        catch (RemoteException e) {
          LOGGER.e("Caught exception", e);
        }
      }
      beaconManager.unbind(this);
    }
  }

  @Override
  public void didEnterRegion(Region region) {
    LOGGER.v("didEnterRegion(region=%s)", region);
    beaconScanningListener.onEnterRegion(region.getUniqueId());
    try {
      beaconManager.startRangingBeaconsInRegion(region);
    }
    catch (RemoteException e) {
      LOGGER.e("Caught Exception", e);
    }
  }

  @Override
  public void didExitRegion(Region region) {
    LOGGER.v("didExitRegion(region=%s)", region);
    beaconScanningListener.onExitRegion(region.getUniqueId());
    try {
      beaconManager.stopRangingBeaconsInRegion(region);
    }
    catch (RemoteException e) {
      LOGGER.e("Error", e);
    }
  }

  @Override
  public void didDetermineStateForRegion(int state, Region region) {
    LOGGER.v("didDetermineStateForRegion(state=%s, region=%s)", state, region);
  }

  @Override
  public void didRangeBeaconsInRegion(Collection<Beacon> beacons, Region region) {
    LOGGER.v("didRangeBeaconsInRegion(size=%s, region=%s, regionUniqueId=%s)", beacons.size(), region,
        region.getUniqueId());
    if (beacons.size() > 0) {
      beaconScanningListener.onBeaconsInRange(beaconIdentifierFactory.from(beacons, region.getUniqueId()));
    }
  }

  @Override
  public void onBeaconServiceConnect() {
    LOGGER.v("onBeaconServiceConnect()");
    beaconManager.addRangeNotifier(this);
    beaconManager.addMonitorNotifier(this);
    for (Region region : targetedRegions) {
      try {
        beaconManager.startMonitoringBeaconsInRegion(region);
      }
      catch (RemoteException e) {
        LOGGER.e("Caught exception", e);
      }
    }
  }

  @Override
  public Context getApplicationContext() {
    return applicationContext;
  }

  @Override
  public void unbindService(ServiceConnection serviceConnection) {
    LOGGER.v("unbindService()");
    applicationContext.unbindService(serviceConnection);
  }

  @Override
  public boolean bindService(Intent intent, ServiceConnection serviceConnection, int i) {
    LOGGER.v("bindService()");
    return applicationContext.bindService(intent, serviceConnection, i);
  }
}

public class BeaconIdentifier {

  private final String uuid;
  private final int major;
  private final int minor;
  private String regionId;

  public BeaconIdentifier(String uuid, int major, int minor) {
    this.uuid = uuid;
    this.major = major;
    this.minor = minor;
  }

  public int getMinor() {
    return minor;
  }

  public int getMajor() {
    return major;
  }

  public String getUuid() {
    return uuid;
  }

  public String getCallParamRepresentation() {
    return (uuid + "_" + major + "_" + minor).toUpperCase();
  }

  public String getRegionId() {
    return regionId;
  }

  public void setRegionId(String regionId) {
    this.regionId = regionId;
  }

  @Override
  public boolean equals(Object o) {
    if (o != null) {
      if (o instanceof BeaconIdentifier) {
        BeaconIdentifier other = (BeaconIdentifier) o;
        return this == other || (this.uuid.equalsIgnoreCase(other.uuid)
            && this.major == other.major && this.minor == other.minor);
      }
      else {
        return false;
      }
    }
    else {
      return false;
    }
  }

  @Override
  public int hashCode() {
    int result = 17;
    result = 31 * result + (uuid != null ? uuid.toUpperCase().hashCode() : 0);
    result = 31 * result + major;
    result = 31 * result + minor;
    return result;
  }

  @Override
  public String toString() {
    return "BeaconIdentifier{" +
        "uuid='" + uuid + '\'' +
        ", major=" + major +
        ", minor=" + minor +
        ", regionId='" + regionId + '\'' +
        '}';
  }
}

BeaconDataProvider 用作每个应用程序的单个实例;在创建Android应用程序时,它由Dagger 2实例化 . 它有@ApplicationScope生命周期 .

信标扫描首先从Android IntentService开始 foreground mode

beaconDataProvider.setForegroundMode();    
    beaconDataProvider.startScanning(targetedBeacons);

一旦设备进入该区域并检测到信标,信标扫描将切换为 background mode

beaconDataProvider.setBackgroundMode();

起初我以为我使用的Onyx Beacons有问题,但我可以用Kontact IO Beacons重现同样的问题 .

  • 您有什么建议吗?

  • 我错过了使用AltBeacon Android库吗?

谢谢,艾琳

3 回答

  • 1

    调用 didExitRegion() 的根本原因是在过去的10秒内Android蓝牙堆栈没有收到与该区域匹配的BLE信标广告包 . (注意:此值可使用 BeaconManager.setRegionExitPeriod(...) 进行配置 . )

    有几件事可能导致这些虚假的 didExitRegion() 调用:

    • 灯塔的播放频率不够高 .

    • 信标是用非常低的无线电信号做广告 .

    • 附近的无线电噪声太大,无法进行可靠的检测 .

    • 接收设备的蓝牙天线设计较差,导致无法检测到较弱的信号 .

    • 接收设备太远而无法可靠地检测到信标 .

    • foregroundScanPeriod或backgroundScanPeriod设置得太短,无法获得有保证的检测

    鉴于您所描述的设置,我怀疑当您以1Hz的频率发送信标时,1-4的某些组合会导致问题 . 您将不得不尝试每个变量,看看是否可以将问题隔离到一个主要问题 . 但同样,不止一个人可能在同一时间发挥作用 .

    了解即使在良好的条件下,通过空中传输的信标数据包中只有80-90%是由典型的Android设备接收的 . 因此,如果你有一个设置,通常在10秒的时间内只收到1-5个信标数据包,如果你运气不好并且连续的几个数据包被无线电噪声破坏,你有时仍会得到退出事件 . 没有办法保证不会发生这种情况 . 通过设置系统,你可以在统计上更加不可能,因此在标称条件下,它会在10秒内收到尽可能多的数据包,因此这种可能性更小 .

    提高广告费率是解决此问题的最简单方法,因为它为您提供了在任何10秒内检测到数据包的更多统计机会 . 但正如您所看到的,在电池寿命方面存在权衡 .

    如果你想保留电池寿命,但不关心获取didExitRegion回调所需的时间,那么你可能想要将 BeaconManager.setRegionExitPeriod(...) 修改为30,000毫秒或更长时间,直到问题消失为止 .

    以上讨论特定于Android Beacon Library的配置,相同的理论思想适用于任何信标检测框架,包括iOS Core Location . 您有时也会在该框架中看到虚假的退出事件 .

  • 0

    我认为问题在于:

    beaconManager.setForegroundScanPeriod(5000L);
    beaconManager.setForegroundBetweenScanPeriod(10000L);
    

    您通常应将scanPeriod设置为5100 ms或更长时间,因为如果广告信标的传输总是在您开始和停止扫描的边界上,那么它很可能会被遗漏 .

    所以尝试:

    beaconManager.setForegroundScanPeriod(5100L);
    beaconManager.setForegroundBetweenScanPeriod(10000L);
    

    希望能帮助到你 . 如果有效,请告诉我 .

  • 0

    作为这个问题的解决方法,我已经实现了一些额外的逻辑来考虑 didExitRegion() 事件只有在某个时间间隔内没有调用相应的 didEnterRegion() (在我的情况下是5分钟,但这可以调整) .

相关问题