2015年11月24日

iBeacon 偵測範例 - android client


去年七月在一個講座上第一次聽到 ibeacon 這個名詞,那時候還不是這麼熱門 google 到的第一個就是 estimote 這間公司的產品,不過因為訂購過海關還會被 NCC 攔截...所以後面就沒再繼續追蹤。不過今年跟朋友借到 estimote 的 Beacon,終於可以來測試一下

iBeacon

iBeacon 是 Apple 公司提出的一套可用於室內定位系統的協議,可以使一個智慧型手機或其他裝置在一個 iBeacon 基站的感應範圍內執行相應的命令。
使用 BLE ( Bluetooth Low Energy ) 技術,具有低功耗、低延遲、高射頻等特性
iBeacon ID 長度是 20 bytes,包含以下三個內容:
  • UUID (16 bytes)
  • major number (2 bytes)
  • minor number (2 bytes)
依距離可分為:
  • 最近:幾公分
  • 中距:幾公尺
  • 遠距:大於 10 公尺
兼容設備:
  • 藍牙 4.0 的 IOS 設備( iPhone4s 及以上,iPad 第三代及以上,iPad mini 第一代及以上,iPod Touch 第五代 )
  • Android 4.3 及以上( 如:三星 Galaxy S3 / S4 / S4 Mini, 三星 Galaxy Note 2 / 3, HTC One, Google / LG Nexus 7 2013 version / Nexus 4 / Nexus 5, HTC Butterfly, OnePlus One )

estimote Beacon 規格

  • 32-bit ARM® Cortex M0 CPU 包含加速規、溫度感測器
  • 藍芽 4.0
  • 最遠射程 70 公尺 ( 無障礙、干擾情況下,使用建議抓 40~50 公尺 )

測試範例一

使用官方釋出 estimote-sdk-preview.jar 進行偵測,官方範例可以點此下載
建立一個 BeaconManager 物件,並註冊 Listener

public static void Create(NotificationManager notificationMngr,
  Context context, final Intent i) {
 try {
  currentContext = context;

  // Create a beacon manager
  beaconManager = new BeaconManager(currentContext);

  // 設定搜尋間格時間
  beaconManager.setBackgroundScanPeriod(TimeUnit.SECONDS.toMillis(1),
    0);

  // 偵測 beacon 進入
  beaconManager.setMonitoringListener(new MonitoringListener() {
   // ... close to us.
   @Override
   public void onEnteredRegion(Region region, List beacons) {
    Log.e(ConfigUtil.TAG, "onEnteredRegion");
    Log.e(ConfigUtil.TAG, "beacons: " + beacons);
    
    if(beacons!=null && beacons.size()>0){
        if(beacons.get(0).getProximityUUID().equals(ESTIMOTE_PROXIMITY_UUID)){
         Log.i(ConfigUtil.TAG, "same beacon");
         return;
        }
     ESTIMOTE_PROXIMITY_UUID = beacons.get(0).getProximityUUID();
     Log.i(ConfigUtil.TAG, "ESTIMOTE_PROXIMITY_UUID:"+ESTIMOTE_PROXIMITY_UUID);
     sendUpdateProximityUUID(ESTIMOTE_PROXIMITY_UUID);
       }
   }

   // ... far away from us.
   @Override
   public void onExitedRegion(Region region) {
   }
  });
  
  // 在 beacon 範圍中
  beaconManager.setRangingListener(new BeaconManager.RangingListener() {
       @Override public void onBeaconsDiscovered(Region region, final List beacons) {
        Log.e(ConfigUtil.TAG, "setRangingListener");
        Log.e(ConfigUtil.TAG, "Ranged beacons: " + beacons);
        
       if(beacons!=null && beacons.size()>0){
        if(beacons.get(0).getProximityUUID().equals(ESTIMOTE_PROXIMITY_UUID)){
         Log.i(ConfigUtil.TAG, "same beacon");
         return;
        }
     ESTIMOTE_PROXIMITY_UUID = beacons.get(0).getProximityUUID();
     Log.i(ConfigUtil.TAG, "ESTIMOTE_PROXIMITY_UUID:"+ESTIMOTE_PROXIMITY_UUID);
     sendUpdateProximityUUID(ESTIMOTE_PROXIMITY_UUID);
       }
       }
     });
  
  // 建立連線
  beaconManager.connect(new BeaconManager.ServiceReadyCallback() {
   @Override
   public void onServiceReady() {
    try {
     // 開使偵測
     beaconManager.startRanging(ALL_ESTIMOTE_BEACONS);
     beaconManager.startMonitoring(ALL_ESTIMOTE_BEACONS);
    } catch (Exception e) {
     Log.e(ConfigUtil.TAG, "Exception:"+e);
    }
   }
  });
 } catch (Exception e) {
  Log.e(ConfigUtil.TAG, "Exception:"+e);
 }
}
結束時,記得停止偵測與關閉連線

public static void stop() {
 try {
  beaconManager.stopRanging(ALL_ESTIMOTE_BEACONS);
  beaconManager.stopMonitoring(ALL_ESTIMOTE_BEACONS);
  beaconManager.disconnect();
 } catch (Exception e) {
  Log.e(ConfigUtil.TAG, "Exception:"+e);
 }
}
藍芽關閉時,也要記得停止偵測與關閉連線,不然你會看到 XXX 已停止...

public class EstimoteReceiver extends BroadcastReceiver {
 private Intent estimoteServiceIntent;

 // Method called when bluetooth is turned on or off.
 @Override
 public void onReceive(Context context, Intent intent) {
  final String action = intent.getAction();
  if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
   final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
     BluetoothAdapter.ERROR);
   switch (state) {
   case BluetoothAdapter.STATE_TURNING_OFF:
    // When bluetooth is turning off, lets stop estimotes ranging
    if (estimoteServiceIntent != null) {
     //這裡會呼叫上面的 stop()
     context.stopService(estimoteServiceIntent);
     estimoteServiceIntent = null;
    }
    break;
   case BluetoothAdapter.STATE_ON:
    // When bluethooth is turned on, let's start estimotes monitoring
    if (estimoteServiceIntent == null) {
     estimoteServiceIntent = new Intent(context,
       EstimoteService.class);
     context.startService(estimoteServiceIntent);
    }
    break;
   }
  }
 }
}
結論:使用官方釋出 estimote-sdk-preview.jar,只能用 Listener 等待接收事件,測試會出現偵測速度忽快忽慢的問題...

測試範例二

因為官方的 JAR 偵測速度忽快忽慢,所以後面又自己上網找了個 lib 取代官方的 JAR。
使用 Bluetooth LE Library,它已提供 beacon 與一般藍芽設備的辨別方式,可以省掉一些麻煩
一開始先檢查一下藍芽是否開啟

private void checkBluetooth() {
 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
 if (mBluetoothAdapter == null) {
     // 如果裝置不支援藍芽
     Toast.makeText(this, "Device doesn't support bluetooth", Toast.LENGTH_SHORT).show();
     return;
 }
          
 // 如果藍芽沒有開啟
 if (!mBluetoothAdapter.isEnabled()) {
     // 發出一個intent去開啟藍芽,
        Intent mIntentOpenBT = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(mIntentOpenBT, REQUEST_ENABLE_BT);
 }
}
init

private void initServerData() {
 //管理所有藍芽裝置
 mDeviceStore = new BluetoothLeDeviceStore();
 //管理 BluetoothManager/BluetoothManager
 mBluetoothUtils = new BluetoothUtils(this);
 mScanner = new BluetoothLeScanner(mLeScanCallback, mBluetoothUtils);
}
偵測到裝置後的 callback

private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
 @Override
 public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
  synchronized(lockObj){
   final BluetoothLeDevice deviceLe = new BluetoothLeDevice(device, rssi, scanRecord, System.currentTimeMillis());
   mDeviceStore.addDevice(deviceLe);
   final EasyObjectCursor c = mDeviceStore.getDeviceCursor();
   for(int i=0; i<=1){
      //是否偵測到同一個beacon
      if(currentBeaconUUID.equals(iBeacon.getUUID())){
       Log.d(ConfigUtil.TAG, "same beacon");
      }else{
       if(tv_main_msg!=null){
        tv_main_msg.setText(iBeacon.getUUID());
       }
       break;
      }
     }else{
      //不是距離最近的beacon
     }
    }else{
     //not beacon device
    }
   }
  }
 }
};
開始搜尋裝置

private void startScan(){
 final boolean mIsBluetoothOn = mBluetoothUtils.isBluetoothOn();
 final boolean mIsBluetoothLePresent = mBluetoothUtils.isBluetoothLeSupported();
 mDeviceStore.clear();
 mBluetoothUtils.askUserToEnableBluetoothIfNeeded();
 if(mIsBluetoothOn && mIsBluetoothLePresent){
  mScanner.scanLeDevice(-1, true);
 }
}
關閉 app 或偵測到藍芽關閉時,記得停止掃描

private void stopScan(){
 mScanner.scanLeDevice(-1, false);
}
結論:使用 Bluetooth LE Library 真的比官方釋出的 JAR 快很多,但離開 beacon 範圍再返回後 update time 跟 distance 更新有時會很慢,或要靠近 beacon 一點...

Android 5.0 使用 estimote 官方釋出 estimote-sdk-preview.jar 藍芽問題

之前測試 android 4.X 的時候沒問題,但到了 Android 5.0 不知道為什麼就出現以下問題... 不過還好換個 thread 就解決了

05-16 09:43:32.181: E/AndroidRuntime(32313): FATAL EXCEPTION: main
05-16 09:43:32.181: E/AndroidRuntime(32313): Process: com.estimote.notificationstest, PID: 32313
05-16 09:43:32.181: E/AndroidRuntime(32313): java.lang.IllegalArgumentException: This cannot be run on UI thread, starting BLE scan can be expensive
05-16 09:43:32.181: E/AndroidRuntime(32313):  at com.estimote.sdk.internal.Preconditions.checkArgument(Preconditions.java:65)
05-16 09:43:32.181: E/AndroidRuntime(32313):  at com.estimote.sdk.service.BeaconService.checkNotOnUiThread(BeaconService.java:502)
05-16 09:43:32.181: E/AndroidRuntime(32313):  at com.estimote.sdk.service.BeaconService.access$600(BeaconService.java:61)
05-16 09:43:32.181: E/AndroidRuntime(32313):  at com.estimote.sdk.service.BeaconService$InternalLeScanCallback.onLeScan(BeaconService.java:490)
05-16 09:43:32.181: E/AndroidRuntime(32313):  at android.bluetooth.BluetoothAdapter$2.onScanResult(BluetoothAdapter.java:1892)
05-16 09:43:32.181: E/AndroidRuntime(32313):  at android.bluetooth.le.BluetoothLeScanner$BleScanCallbackWrapper$1.run(BluetoothLeScanner.java:330)
05-16 09:43:32.181: E/AndroidRuntime(32313):  at android.os.Handler.handleCallback(Handler.java:739)
05-16 09:43:32.181: E/AndroidRuntime(32313):  at android.os.Handler.dispatchMessage(Handler.java:95)
05-16 09:43:32.181: E/AndroidRuntime(32313):  at android.os.Looper.loop(Looper.java:211)
05-16 09:43:32.181: E/AndroidRuntime(32313):  at android.app.ActivityThread.main(ActivityThread.java:5321)
05-16 09:43:32.181: E/AndroidRuntime(32313):  at java.lang.reflect.Method.invoke(Native Method)
05-16 09:43:32.181: E/AndroidRuntime(32313):  at java.lang.reflect.Method.invoke(Method.java:372)
05-16 09:43:32.181: E/AndroidRuntime(32313):  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1016)
05-16 09:43:32.181: E/AndroidRuntime(32313):  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:811)

References

=======================================================================

2016/08/23 補充

※ 目前已知 "Bluetooth-LE-Library---Android" 可能會出現 Beacon UUID 缺少位數的問題,這部分 "Bluetooth-LE-Library---Android" 已經在 2015/7/15 v1.0.0 版 fix 了,舊版的可以到  Bluetooth-LE-Library---Android 下載更新,或是下載附件 IBeaconManufacturerData.java 並覆蓋 "Bluetooth LE Library\src\uk\co\alt236\bluetoothlelib\device\mfdata" 下 IBeaconManufacturerData.java

PS. 範例二中 IBeaconManufacturerData.java 已更新,可直接使用