AndroidでBLEデバイス名の変更を検出する

Dec 01 2020

デバイスの名前を介してデータを出力する、作業中のBLEデバイスがあります。デバイスは正常に機能しており、nRFConnectなどのアプリを使用して名前が正しく変更されていることを確認できます。しかし、私たちは自分たちのAndroidアプリで同じことをするのに苦労しています。デバイスは正常に検出できますが、元の名前を超えて移動することはほとんどありません。

私が始めたコードには、BluetoothLeScannerとstartScan()関数を使用してスキャンするonResume()で開始されるループがあります。

public void BLEScan(final boolean enable){

    final int SCAN_PERIOD = 12000;
    final BluetoothLeScanner bluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();

    if (enable) {
        Log.d(TAG, "Starting Scan");
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                BLEScan(false);
                invalidateOptionsMenu();
            }
        }, SCAN_PERIOD);
        
        bluetoothLeScanner.flushPendingScanResults(mLeScanCallback);
        bluetoothLeScanner.startScan(mLeScanCallback);
    } else {
        Log.d(TAG, "Stopping Scan");
        bluetoothLeScanner.flushPendingScanResults(mLeScanCallback);
        bluetoothLeScanner.stopScan(mLeScanCallback);
        
        mHandler.removeCallbacksAndMessages(null);

        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                BLEScan(true);
            }
        }, SCAN_PERIOD);
    }
}

また、デバイスに基づいてフィルタリングすることがmLeScanCallbackで検出されます。基本的に、これまでにデバイスを見たことがない場合は、ビーコンのリストに追加します。ただし、以前に確認したことがある場合は、ビーコンのseen()関数を使用して値を更新し、情報の取得元となるビーコンの名前を使用します。どちらの場合も、アダプターを更新して新しい情報を入力します。

private ScanCallback mLeScanCallback =
    new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            final ScanResult res2 = result;
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    BluetoothDevice device = res2.getDevice();
                    String address = device.getAddress();
                    
                    if (device.getName() != null){
                        if (device.getName().contains("ABT:")){
                            if (!mLeBeacons.containsKey(address)){
                                BleBeacon beacon = new BleBeacon(device.getName(), address);
                                bleList.add(beacon);
                                adapter.notifyDataSetChanged();

                                mLeBeacons.put(device.getAddress(), beacon);
                            } else {
                                for (int x = 0; x < bleList.size(); x++){
                                    if (device.getAddress().equals(bleList.get(x).getMAC())){
                                        bleList.get(x).seen(device.getName());
                                        adapter.notifyDataSetChanged();
                                    }
                                }
                            }
                        }
                    }
                }
            });
        }
    };

ただし、名前を更新した後でも、mleScanCallbackは元の名前のみを返します。

ここを検索した後、私が見つけ続けたのは、関数fetchUuidsWithSdp()と、ACTION_FOUND、ACTION_UUID、ACTION_DISCOVERY_FINISHED、ACTION_NAME_CHANGEDなどのインテントを使用して、名前が正しく変更されていることを確認することでした。そこで、mLeScanCallbackにfetchUuidsWithSdp()を追加しました。ただし、これによりインテントがトリガーされますが、名前はまだ更新されていません。実際のインテントでfetchUuidsWithSdp()を呼び出してみましたが、それも役に立ちませんでした。不思議なことに、携帯電話の画面をオフにしたり、BLEデバイスから十分に離れたりすると、ACTION_NAME_CHANGEDが起動することがありました。ただし、onPause()が行うのは呼び出しsuper.onPause()BLEScan(false)。だけです。そして、これらは私がすでにループで行っていたものだったので、それが起きている間にこれをコードに組み込む方法がわかりませんでした。

さらに検索したところ、fetchUuidsWithSdq()関数を使用するには、BluetoothAdapterのstartDiscovery()関数を使用する必要があることがわかりました。そこで、BLEScanを使用するように変更し、mLeScanCallbackを完全に削除しました。

public void BLEScan(final boolean enable){
    final int SCAN_PERIOD = 12000;
    if (enable) {
        Log.d(TAG, "Starting Scan");
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                BLEScan(false);
                invalidateOptionsMenu();
            }
        }, SCAN_PERIOD);

        if (mBluetoothAdapter.isDiscovering()){
            mBluetoothAdapter.cancelDiscovery();
        }
        mBluetoothAdapter.startDiscovery();
    } else {
        Log.d(TAG, "Stopping Scan");
        mBluetoothAdapter.cancelDiscovery();

        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                BLEScan(true);
            }
        }, SCAN_PERIOD);
    }
}

ただし、インテントが以前に起動していたときは、インテントフィルタに含まれていたとしても、現在は起動していません。さらに検索を行ったところ、startDiscovery()はLeデバイスでは機能しないと言う人がいました。そこで、代わりに使用することになっているものを検索しました...これにより、BluetoothLeScannerを使用したstartScanに戻ることができました。その時点で、私は自分が輪になって助けが必要だと気づきました。

どこかで何かを逃したので、私は自分のプロセス全体を調べました。どこにあったのかわかりません。startScan()を使用する必要がありますか?startDiscovery()を正しく使用していませんか?それとも私が完全に使用すべき何か他のものはありますか?ACTION_NAME_CHANGEDが発生することがあるため、その状態に戻りたいと思うことがありますが、デバイスが起動しているときに常に動作させるにはどうすればよいですか?

回答

M.Kotzjan Dec 02 2020 at 21:16

Androidでのキャッシュに問題があるだけかもしれません。考えられる解決策については、こちらの回答を参照してください。https://stackoverflow.com/a/50745997/7473793

Seth Dec 03 2020 at 23:48

元のコードで何が間違っているのかはわかりませんでしたが(Androidはこの点で壊れているように聞こえますが)、回避策を見つけました。nRFのNFCToolboxには、ソースコードが用意されており、フィルターが追加されたバッチでスキャンされるほか、北欧のライブラリにある独自のLeスキャナーもあります。スキャン機能は次のようになります。

    public void BLEScan(final boolean enable){

        final BluetoothLeScannerCompat bluetoothLeScanner = BluetoothLeScannerCompat.getScanner();
        final ScanSettings settings = new ScanSettings.Builder()
                .setLegacy(false)
                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).setReportDelay(1000).setUseHardwareBatchingIfSupported(false).build();
        final List<ScanFilter> filters = new ArrayList<>();
        ParcelUuid Uuid = new ParcelUuid(UUID.fromString(uuidString));
        filters.add(new ScanFilter.Builder().setServiceUuid(Uuid).build());

        if (enable) {
            Log.d(TAG, "Starting Scan");
            // Stops scanning after a pre-defined scan period.
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    BLEScan(false);
                    invalidateOptionsMenu();
                }
            }, SCAN_PERIOD);

            bluetoothLeScanner.startScan(filters, settings, mLeScanCallback);
        } else {
            BLEService.refreshGatt();
            Log.d(TAG, "Stopping Scan");
            bluetoothLeScanner.stopScan(mLeScanCallback);

            mHandler.removeCallbacksAndMessages(null);

            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    BLEScan(true);
                }
            }, SCAN_PERIOD);
        }
    }

そして、私のコールバックは次のようになります。

    private no.nordicsemi.android.support.v18.scanner.ScanCallback mLeScanCallback =
            new no.nordicsemi.android.support.v18.scanner.ScanCallback() {
                @Override
                public void onBatchScanResults(@NonNull final List<no.nordicsemi.android.support.v18.scanner.ScanResult> results) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            for (final no.nordicsemi.android.support.v18.scanner.ScanResult result : results){
                                BluetoothDevice device = result.getDevice();

                                if (device != null){
                                    String address = device.getAddress();
                                    String name = result.getScanRecord() != null ? result.getScanRecord().getDeviceName() : null;

                                    if (!mLeBeacons.containsKey(address)) {
                                        BleBeacon beacon = new BleBeacon(device.getName(), address);
                                        bleList.add(beacon);
                                        adapter.notifyDataSetChanged();

                                        mLeBeacons.put(device.getAddress(), beacon);
                                    } else {
                                        for (int x = 0; x < bleList.size(); x++){
                                            if (device.getAddress().equals(bleList.get(x).getMAC())){
                                                bleList.get(x).seen(device.getName());
                                                adapter.notifyDataSetChanged();
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    });
                }

それはトリックをするようでした。ただし、依存関係に以下を追加する必要があります。

implementation 'no.nordicsemi.android.support.v18:scanner:1.4.2'

その後、私の名前ACTION_NAME_CHANGEインテントが適切にトリガーされ、データが更新されます。

結果から名前を取得する別の方法なのか、それともバッチスキャンなのかはわかりません。ただし、Nordicでさえ標準のAndroid BLEライブラリを使用していない場合は、これが最善の方法だと思います。