Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
212 views
in Technique[技术] by (71.8m points)

android - How to detect when a BLE device is not in range anymore?

I use a LeScanCallback (can not use the newer scan methods because I'm developing for api 18. Not that it matters, since the android 5.0+ apis don't offer this functionality either) to detect when a nearby BLE device is detected:

private BluetoothAdapter.LeScanCallback bleCallback = new BluetoothAdapter.LeScanCallback() {

    @Override
    public void onLeScan(BluetoothDevice bluetoothDevice, int i, byte[] bytes) {
        discoveredDevices.add(bluetoothDevice);
    }
};

I am not pairing or connecting with the devices because that's not required, I simply want to see which devices are nearby.

I'm trying to make a service that, every 5 mins or so, calls a webserver to update which devices are nearby at that moment.

Tricky part is that the android device will be moving, so a bluetooth device that is nearby right now, might not be in 5 mins. In that case I need to remove it from discoveredDevices.

Ideally, I would like to receive a callback when a bluetooth device was in range before, but is not anymore. This callback doesn't exist though.

(I'm aware of the android.bluetooth.device.action.ACL_CONNECTED and android.bluetooth.device.action.ACL_DISCONNECTED broadcasts, but those are for when you connect to a bluetooth device, which I don't want.)

An option is to do a fresh scan every 5 mins, but you can't tell when all nearby devices have been discovered, so you would have to do a timed scan, e.g. scan for 5 seconds and then send the collected data to the webservice.
This sounds dirty and risky because you can never know for sure all nearby devices were discovered within the allotted time, so I would very much like to avoid doing it like that.

Is there another way to do this?


Edit
Some devices continuously report discovery of nearby bluetooth devices, even if they were already discovered before. If that functionality was universal I could solve my problem, however this is device specific.

My phone's bluetooth adapter for example only discovers nearby devices once. Some other devices I have tested with do continuously report the same nearby devices, but not all devices do, so I can't rely on that unfortunately.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

This sounds dirty and risky because you can never know for sure all nearby devices were discovered within the allotted time, so I would very much like to avoid doing it like that.

That sounds like a reasonable assumption, but it's wrong.

Bluetooth low energy works in a particular way and BLE devices have some limits. For instance, they have a fixed range of possible advertising frequencies, ranging from 20 milliseconds to 10.24 seconds, in steps of 0.625 milliseconds. See here and here for more detailed information.

This means that it can take at most 10.24 seconds before a device will broadcast a new advertisement package. BLE devices generally, if not always, provide a way for their owner to adjust their advertising frequency, so the frequency can of course vary.

In cases where you are periodically collecting data about nearby devices, like yours, it is fine to use a scan with a fixed time limit, save that data somewhere, restart the scan, collect new data, compare with old data --> get results.

For example, if a device was found in scan 1 but not in scan 2, you can conclude that the device was in range, but is not anymore.
Same goes for the other way around: if a device was found in scan 4 but not in scan 3, it is a newly discovered device.
Finally, if a device was found in scan 5, was not found in scan 6, but was again found in scan 7, it is rediscovered and can be handled as such if need be.


Because I'm answering my own question here, I'll add the code that I used to implement this.

I have the scanning done in a background service, and communicate to other parts of the app using BroadcastReceivers. Asset is a custom class of mine that holds some data. DataManager is a custom class of mine that - how did you guess it - manages data.

public class BLEDiscoveryService extends Service {

    // Broadcast identifiers.
    public static final String EVENT_NEW_ASSET = "EVENT_NEW_ASSET ";
    public static final String EVENT_LOST_ASSET = "EVENT_LOST_ASSET ";

    private static Handler handler;
    private static final int BLE_SCAN_TIMEOUT = 11000; // 11 seconds

    // Lists to keep track of current and previous detected devices.
    // Used to determine which are in range and which are not anymore.
    private List<Asset> previouslyDiscoveredAssets;
    private List<Asset> currentlyDiscoveredAssets;

    private BluetoothAdapter bluetoothAdapter;

    private BluetoothAdapter.LeScanCallback BLECallback = new BluetoothAdapter.LeScanCallback() {

        @Override
        public void onLeScan(BluetoothDevice bluetoothDevice, int i, byte[] bytes) {

            Asset asset = DataManager.getAssetForMACAddress(bluetoothDevice.getAddress());
            handleDiscoveredAsset(asset);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();

        BluetoothManager manager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
        bluetoothAdapter = manager.getAdapter();

        previouslyDiscoveredAssets = new ArrayList<>();
        currentlyDiscoveredAssets = new ArrayList<>();

        handler = new Handler();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // Start scanning.
        startBLEScan();

        // After a period of time, stop the current scan and start a new one.
        // This is used to detect when assets are not in range anymore.
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                performRepeatingTask();

                // Repeat.
                handler.postDelayed(this, BLE_SCAN_TIMEOUT);
            }
        }, BLE_SCAN_TIMEOUT);

        // Service is not restarted if it gets terminated.
        return Service.START_NOT_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        handler.removeCallbacksAndMessages(null);
        stopBLEScan();

        super.onDestroy();
    }

    private void startBLEScan() {
        bluetoothAdapter.startLeScan(BLECallback);
    }

    private void stopBLEScan() {
        bluetoothAdapter.stopLeScan(BLECallback);
    }

    private void handleDiscoveredAsset(Asset asset) {
        currentlyDiscoveredAssets.add(asset);

        // Notify observers that we have a new asset discovered, but only if it was not
        // discovered previously.
        if (currentlyDiscoveredAssets.contains(asset) &&
                !previouslyDiscoveredAssets.contains(asset)) {
            notifyObserversOfNewAsset(asset);
        }
    }

    private void performRepeatingTask() {
        // Check if a previously discovered asset is not discovered this scan round,
        // meaning it's not in range anymore.
        for (Asset asset : previouslyDiscoveredAssets) {
            if (!currentlyDiscoveredAssets.contains(asset)) {
                notifyObserversOfLostAsset(asset);
            }
        }

        // Update lists for a new round of scanning.
        previouslyDiscoveredAssets.clear();
        previouslyDiscoveredAssets.addAll(currentlyDiscoveredAssets);
        currentlyDiscoveredAssets.clear();

        // Reset the scan.
        stopBLEScan();
        startBLEScan();
    }

    private void notifyObserversOfNewAsset(Asset asset) {
        Intent intent = new Intent();
        intent.putExtra("macAddress", asset.MAC_address);
        intent.setAction(EVENT_NEW_ASSET);

        sendBroadcast(intent);
    }

    private void notifyObserversOfLostAsset(Asset asset) {
        Intent intent = new Intent();
        intent.putExtra("macAddress", asset.MAC_address);
        intent.setAction(EVENT_LOST_ASSET);      

        sendBroadcast(intent);
    }
}

This code is not perfect and might even be buggy, but it will at least give you an idea or example of how this can be implemented.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...