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
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 here
ain.
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");
}
}
}
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.