Ble扫描回调仅被调用几次然后停止

10
我有两部安卓5.0.2的手机,它们都安装了最新的Radius Beacon应用程序:Locate Beacon。同时,我打开了两个iBeacon发送器,并使用该应用程序在两台手机上看到RSSI不断变化。
但当我尝试编写一些示例代码来模拟上述情况时,我发现BLE扫描回调总是在调用2或3次后停止调用。我最初怀疑“Locate Beacon”可能使用了不同的方法,因此我尝试了两种API,一种是旧版本4.4的API,另一种是Android 5中引入的新方法,但两者都表现相同(但都运行在安卓5上)。
4.4的API:
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);
}}

还有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);
}}

这些功能非常简单,只在用户界面显示计数器,并最终始终停在2或3。

我之前在一台装有Android 4.4的三星Note 2上尝试过接收BLE广告,它表现得非常完美,回调函数每秒钟被调用一次。 那么有人可以帮忙吗?为什么Radius' Locate Beacon在这里能够很好地工作?

2个回答

22
不同的Android设备在扫描可连接的BLE广告时表现不同。在某些设备上(例如旧的Nexus 4),扫描API只会在发送可连接广告的发射器上获得一个回调,而对于非可连接广告,它们会为每个广告获得一个扫描回调。较新的设备(例如Nexus 5和大多数2015年后建造的设备)无论广告是否可连接,都会提供每个广告的扫描回调。
您提到的Locate应用程序及其替代应用程序Beacon Scope使用开源的Android Beacon Library来检测信标。它是基于您在问题中展示的相同扫描API构建的,但通过定义一个扫描周期(默认情况下为前台1.1秒)并在此间隔内停止和重新启动扫描来解决了这个问题。停止和重新启动扫描会导致Android发送一个新的回调。
这里还有一些其他注意事项:
这个问题涉及到4.x和5.x扫描API的可连接设备多次扫描回调的问题。
目前尚不清楚在不同设备上提供可连接广告的扫描回调的差异是由于Android固件差异还是蓝牙硬件芯片组差异。
目前似乎没有办法检测设备是否需要重新启动扫描以获取可连接广告的额外回调,因此如果您的目标是各种设备,您需要计划停止和重新启动扫描。
使用Android的原始扫描API是了解BLE信标工作原理的好方法。但是与BLE信标一起工作存在许多复杂性(这只是一个例子),这就是为什么使用像Android Beacon Library这样的SDK是一个不错的选择,可以避免让您抓狂。
完全透明地说,我是Locate应用程序的作者,也是Android Beacon Library开源项目的首席开发人员。

感谢您对此进行了澄清,关于每1.1秒打开/关闭的问题,我理解为在循环中使用BluetoothLeScanner.stopScan();Thread.Sleep(1100);BluetoothLeScanner.startScan(),是这样吗? - Shawn
你需要开始扫描,暂停1100毫秒,然后停止(与所示顺序相反)。此外,Thread.sleep() 不是 Android 编程的最佳方法。使用 Handler 类或 AlarmManager 类来调度未来代码执行是首选方法。 - davidgyoung
@davidgyoung 还有一个问题需要您的帮助,虽然原始问题发布已经很久了。在测试中,我注意到几乎每个“停止和启动”周期(将4个IBeacon放在手机附近),扫描到的数据都是不完整的,这意味着有时只扫描到1或2个IBeacon,有时什么也没有,很少发生在一个周期内扫描到4个IBeacon,这对于Android设备来说是常见的还是由于“停止和启动”的副作用引起的?我记得在我的Android4.4三星NOTE2上(BLE扫描运行得非常完美,无需停止和启动),周围的IBeacon可以持续扫描并发布。谢谢! - Shawn
这可能值得新开一个问题。停止和启动扫描可能会影响检测,但问题更有可能是信标广告频率、发射器功率或接收机无线电方面的问题。您能否发布一个新问题,告知信标型号(和广告频率)以及手机型号的详细信息? - davidgyoung
有人找到解决办法了吗?我正在开发一个蓝牙产品,但在Wiko Birdy上遇到了这个问题。由于另一个设备正在广播范围内,我的设备从未被扫描检测到。 - Jissay
显示剩余3条评论

-1
David - 你确定扫描回调函数会为每个非可连接广告调用吗?我有一部小米红米3和另一部运行Android 6.0的Nexus 5手机。我有一个BLE传感器,每隔1分钟发送一次数据。这些作为中央BLE设备出现的手机应该接收并处理来自传感器的数据。我可以从OTA BLE捕获设备中看到传感器每隔1分钟发送数据。然而,两部手机似乎在1分钟间隔下处理数据几分钟后停止处理4-6分钟,然后重新开始处理。
手机处理时间间隔如下: 1分钟,2分钟,3分钟,8分钟,9分钟,10分钟,11分钟 因此,在处理3个1分钟间隔的数据包后,任何一部手机都将停止处理4-6分钟。
以下是执行处理的代码。
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");
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接