9

I have 2 phones with Android 5.0.2, they both installed the latest Radius Beacon's App: Locate Beacon, meanwhile, I turned on 2 IBeacon sender, and can see the RSSI keep changing in both phone with the App.

But when I tried to write some sample code to simulate above situation, I found the ble scan callback always stop get called after called 2 or 3 times, I initially suspect the 'Locate Beacon' may use different way, so I tried with 2 kinds of API, one is for old 4.4, and another is the new way introduced in android 5, but both the same behavior(but all running on android 5).

the 4.4 one:

public class MainActivity extends Activity {
private BluetoothAdapter mBluetoothAdapter;
private static final String LOG_TAG = "BleCollector";
private TextView calledTimesTextView = null;
private int calledTimes = 0;
// Device scan callback.
private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(final BluetoothDevice device, int rssi,
            byte[] scanRecord) {
        calledTimes++;
        runOnUiThread(new Runnable() {
            @Override
            public void run() {

                calledTimesTextView.setText(Integer.toString(calledTimes));
            }
        });
        Log.e(LOG_TAG, "in onScanResult, " + " is coming...");
    }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    calledTimesTextView = (TextView) findViewById(R.id.CalledTimes);
    mBluetoothAdapter = ((BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE))
            .getAdapter();
    mBluetoothAdapter.startLeScan(mLeScanCallback);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();
    if (id == R.id.action_settings) {
        return true;
    }
    return super.onOptionsItemSelected(item);
}}

And the 5.0.2:

public class MainActivity extends Activity {
private BluetoothAdapter mBluetoothAdapter = null;
private BluetoothLeScanner mLescanner;
private ScanCallback mLeScanCallback;
private static final String LOG_TAG = "BleFingerprintCollector";
private TextView calledTimesTextView = null;
private int calledTimes = 0;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    calledTimesTextView = (TextView) findViewById(R.id.CalledTimes);
    this.mBluetoothAdapter = ((BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE))
            .getAdapter();
    this.mLescanner = this.mBluetoothAdapter.getBluetoothLeScanner();

    ScanSettings bleScanSettings = new ScanSettings.Builder().setScanMode(
            ScanSettings.SCAN_MODE_LOW_LATENCY).build();

    this.mLeScanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            calledTimes++;
            runOnUiThread(new Runnable() {
                @Override
                public void run() {

                    calledTimesTextView.setText(Integer
                            .toString(calledTimes));
                }
            });
            Log.e(LOG_TAG, "in onScanResult, " + " is coming...");
        }

        @Override
        public void onBatchScanResults(List<ScanResult> results) {

        }

        @Override
        public void onScanFailed(int errorCode) {
        }
    };
    this.mLescanner.startScan(null, bleScanSettings, this.mLeScanCallback);

}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();
    if (id == R.id.action_settings) {
        return true;
    }
    return super.onOptionsItemSelected(item);
}}

They are very simple and just show a counter in UI, proved finally always stopped at 2 or 3.

I've played this ble advertising receiving before on a SamSung note 2 with android 4.4 device, it works perfectly, the callback get called every second. then anyone can help? why Radius' Locate Beacon works well here?

Shawn
  • 652
  • 1
  • 7
  • 29

2 Answers2

19

Different Android devices behave differently when scanning for connectable BLE advertisements. On some devices (e.g. the Nexus 4), the scanning APIs only get one callback per scan for transmitters sending a connectable advertisement, whereas they get a scan callback for every advertisement for non-connectable advertisements. Other devices (e.g. the Nexus 5) provide a scan callback every single advertisement regardless of whether it is connectable.

The Locate app you mention uses the open source Android Beacon Library to detect beacons. It is built on top of the same scanning APIs you show in your question, but it gets around this problem by defining a scan period (1.1 seconds by default in the foreground) and stopping and restarting a scan at this interval. Stopping and restarting the scan causes Android to send a new callback.

A few other notes here:

  • This issue of getting multiple scan callbacks for connectable devices applies to both the 4.x and 5.x scanning APIs.

  • It is unclear whether the difference in delivering scan callbacks for connectable advertisements on different devices is due to Android firmware differences or bluetooth hardware chipset differences.

  • There doesn't seem to be a way to detect if a device requires a scan restart to get additional callbacks for connectable advertisements, so if you are targeting a wide variety of devices, you need to plan to stop and restart scanning.

  • Using Android's raw scanning APIs is a great way to understand how BLE beacons work. But there are lots of complexities with working with BLE beacons (this is just one example) which is why using a SDK like the Android Beacon Library is a good choice to keep you from pulling your hair out.

Full disclosure: I am the author of the Locate app in the lead developer on the Android Beacon Library open source project.

davidgyoung
  • 59,109
  • 12
  • 105
  • 181
  • thanks for great help to clarify this, regarding that turn on/off by 1.1 seconds interval, i understand it as `BluetoothLeScanner.stopScan();Thread.Sleep(1100);BluetoothLeScanner.startScan()` in a loop, correct? – Shawn Apr 23 '15 at 08:37
  • You need to start the scan, pause for 1100ms, then stop (the opposite order to what is shown.). Also, Thread.sleep() is not the best approach for Android programming. Using the Handler class or the AlarmManager class to schedule future code execution is the preferred approach. – davidgyoung Apr 23 '15 at 12:03
  • A colleague and me tried this on the Sony Xperia Z and Z3 Compact, **both with Android 5.1.1**. On Z3 the callback worked constantly while on Z it only scanned a few times and then stopped. Seems like the differences are located on the hardware level. – Big_Chair Sep 11 '15 at 10:53
  • @davidgyoung one more issue need you help though it's quite long since the original question posted. In testing, I noticed almost in each cycle of `stop and start` (put 4 IBeacon near Phone), the scanned data is incomplete, which means sometimes 1 or 2 IBeacon scanned, sometimes nothing, rarely happened that 4 IBeacon scanned in one cycle, is this common for Android device or is caused by side effect of `stop and start`? I remembered in my Android4.4 Samsung NOTE2 (the BLE scan running perfectly without need stop and start), the IBeacon around can constantly scanned and posted. thx! – Shawn Nov 20 '15 at 00:45
  • This is probably wothy of a new question. Stopping and starting scanning can affect detections, but the problem is more likely the beacon advertising rate or transmitter power or the receiver radio. Can you post a new question telling the beacon model (and advertising rate) and the phone model details? – davidgyoung Nov 20 '15 at 07:07
  • Did someone find a solution to this ? I'm working on a bluetooth product, and I just get this issue with a Wiko Birdy. My device is never detected by the scan because there is another one advertising in range. – Jissay Dec 16 '15 at 13:29
  • @Jissay, that sounds like a different issue. Two beacons with different mac addresses should be detected independently on all android devices as far as I know. I would open a new question with the details of what you are seeing. – davidgyoung Dec 16 '15 at 14:02
  • @davidgyoung nevermind I just tried to get this issue again but it didn't came up (that's a pretty good news). It comes from somewhere in my code I think, thanks anyway :) – Jissay Dec 16 '15 at 14:08
-1

David - Are you sure that scan callback gets called for every non-connectable advertisement. I have a Xiaomi Redmi 3 and another Nexus 5 phone running Android 6.0. I have a BLE sensor that at every 1 minute interval sends the data. These phones appearing as central BLE device should receive and process the data from the sensor. I can see from an Over the Air (OTA) BLE capture device that it the sensor is sending data every 1 minute. However both phones seems to process data for few minutes at 1 minute interval but after that stop processing for 4 - 6 minutes and then start processing agenter code hereain. Time interval of phone processing on looks like this 1 min, 2 min, 3 min, 8min, 9min, 10min, 11 min So after processing 3 packets at 1 minute interval, either phone will stop processing for 4 -6 minutes.

Here is code that does the processing.

public class BluetoothDataReader {
    private final Context context;

    public BluetoothDataReader(Context context) {
        this.context = context;
    }

    public void startReading() {
        BluetoothAdapter btAdapter = getBluetoothAdapter();
        if (btAdapter == null) return;

        BluetoothLeScanner scanner = btAdapter.getBluetoothLeScanner();
        ScanSettings settings = new ScanSettings.Builder()
                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                .build();
        scanner.startScan(Collections.<ScanFilter>emptyList(), settings, new ScanRecordReader());
    }

    public void uploadScanBytes(SensorDataUploader sensorDataUploader, int count) {
        BluetoothAdapter btAdapter = getBluetoothAdapter();
        if (btAdapter == null) return;

        BluetoothLeScanner scanner = btAdapter.getBluetoothLeScanner();
        ScanSettings settings = new ScanSettings.Builder()
                .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
                .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
                .build();
 //       scanner.startScan(Arrays.asList(new ScanFilter.Builder().setDeviceAddress("26:50:26:50:26:50").build()), settings, new LimitedScanRecordReader(sensorDataUploader, count, scanner));
           scanner.startScan(Collections.<ScanFilter>emptyList(), settings, new LimitedScanRecordReader(sensorDataUploader, count, scanner));
    }

    @Nullable
    private BluetoothAdapter getBluetoothAdapter() {
        BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
        if(btAdapter == null){
            Log.i(BluetoothDataReader.class.getName(), "No bluetooth adapter available");
            return null;
        }

        if(!btAdapter.isEnabled()){
            Log.i(BluetoothDataReader.class.getName(), "Enable bluetooth adapter");
            Intent enableBluetooth = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            context.startActivity(enableBluetooth);
        }
        return btAdapter;
    }

    private class LimitedScanRecordReader extends ScanCallback {
        private final int limit;
        private final BluetoothLeScanner scanner;


        private int scanRecordRead = 0;
        private final SensorDataUploader sensorDataUploader;

        private LimitedScanRecordReader( SensorDataUploader sensorDataUploader, int limit, BluetoothLeScanner scanner) {
            this.limit = limit;
            this.scanner = scanner;
            this.sensorDataUploader = sensorDataUploader;
        }

        @Override
        public void onScanResult(int callbackType, ScanResult result) {
//            if(scanRecordRead++ < limit) {
   //         if(result.getDevice().getAddress().equals("A0:E6:F8:01:02:03")) {
   //         if(result.getDevice().getAddress().equals("C0:97:27:2B:74:D5")) {

            if(result.getDevice().getAddress().equals("A0:E6:F8:01:02:03")) {
                long timestamp = System.currentTimeMillis() -
                        SystemClock.elapsedRealtime() +
                        result.getTimestampNanos() / 1000000;



                byte[] rawBytes = result.getScanRecord().getBytes();
                Log.i(DataTransferService.class.getName(), "Raw bytes: " + byteArrayToHex(rawBytes));
                sensorDataUploader.upload(timestamp, rawBytes);
            }
//            }else {
//                scanner.stopScan(this);
//            }
        }
        public String byteArrayToHex(byte[] a) {
            StringBuilder sb = new StringBuilder(a.length * 2);
            for(byte b: a)
                sb.append(String.format("%02x", b & 0xff));
            return sb.toString();
        }

        public void onScanFailed(int errorCode) {
            Log.i(DataTransferService.class.getName(), "Error code is:" + errorCode);
        }

        public void onBatchScanResults(java.util.List<android.bluetooth.le.ScanResult> results) {
            Log.i(DataTransferService.class.getName(), "Batch scan results");
        }
    }

    private class ScanRecordReader extends ScanCallback {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            byte []rawBytes = result.getScanRecord().getBytes();
            Log.i(DataTransferService.class.getName(), "Raw bytes: " + byteArrayToHex(rawBytes ));
//            Map<ParcelUuid, byte[]> serviceData = result.getScanRecord().getServiceData();
//            for(ParcelUuid uuid : serviceData.keySet()) {
//                Log.i(DataTransferService.class.getName(), uuid.toString() + ":" +  byteArrayToHex(serviceData.get(uuid)));
//            }
//            Log.i(DataTransferService.class.getName(),result.toString());
        }
        public String byteArrayToHex(byte[] a) {
            StringBuilder sb = new StringBuilder(a.length * 2);
            for(byte b: a)
                sb.append(String.format("%02x", b & 0xff));
            return sb.toString();
        }

        public void onScanFailed(int errorCode) {
            Log.i(DataTransferService.class.getName(), "Error code is:" + errorCode);
        }

        public void onBatchScanResults(java.util.List<android.bluetooth.le.ScanResult> results) {
            Log.i(DataTransferService.class.getName(), "Batch scan results");
        }
    }
}