Selfad logo

BLE data and file transfer on Android

There's a lot of confusion on how data should be transferred between two BLE devices. Perhaps the BLE is quite complicated for those with no prior experience with it. Moreover, it's quite depressing even for those who have spent several years with such devices. This is quite true because there's a wide variety of sensors and other BLE devices that don't work according to the standards. However, here we provide an example that may be considered as a generic guideline for engineering Android BLE data transfer apps. The intention is to illustrate how a file or data is exhanged between an Android device and a BLE capable peripheral. The BLE peripheral sends data and the app in Android receives it. The file size may be from a few bytes to hundreds of megabytes altough the max datarate is around 20 kbytes / second.


Scanning for our BLE device

The scanning performance is very poor with most Android devices if the default settings are used. It's recommend to use the Scansettings.SCAN_MODE_LOW_LATENCY if the user needs to select a device to connect from the UI. This shouldn't be running in the background at all times as it consumes a lot of power. The code below illustrates how the devices are quickly discovered:

 
    private void btStartDeviceScan() { 
        startScanUI(); 
        AsyncTask.execute(new Runnable() { 
            @Override 
            public void run() { 
                if (btScanner != null && btAdapter.isEnabled()) { 
                    ScanSettings settings = new ScanSettings.Builder() 
                            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 
                            .build(); 
                    btScanner.startScan(null, settings, leScanCallback); 
                } 
            } 
        }); 
 
        if (btScanner == null) { 
            stopScanUI(); 
        } 
    }

Once a scan result has been found, we look for its contents. It's possible to utilize the hardware assisted filters but they're not available on all Android devices so let's just look for all scan results and do the filtering in our software instead. Here we have the leScanCallback() and the processScanResult() that investigate the results. We look for our service SERVICE_PARCEL_UUID. Please notice that it's important to stop scanning before connecting to the device. Otherwise the scan may be continuing its work meanwhile.


Connecting to the device

In fact, we're searching for our SERVICE_UUID. The uuid UUID used in this example is utilized elsewhere (on other projects) in serial connection services but the 128 bit UUID should be unique for the particular service so do not blindly copy it for other than testing purposes. If you're building your own service you'd need to use an arbitrary 128 bit UUID. Of course, you can go and register your service at bluetooth.com and get a 16 bit UUID but it would cost some time and money. Nordic Semiconductor's NRF Connect (Android app) may be used to get an overall view of the UUID topologies. In short, there's a service (services are collections of characteristics) UUID, the service characteristics UUID(s), for example, one for RX and another for TX, and a way to configure the service characteristics (configuration UUID(s)), one for each configurable characteristics.

The BLE terms aren't very informative for all programmers. Some analogies may be drawn in the following way:

Service UUID: This is what you advertise to others, a reference to your set of "functions". A unique value.
Service Characteristics UUIDs: The "functions" what your service provides, for example RX and TX
Client Characteristics Configuration Descriptor UUIDs: The "switches" that make the Service Characteristics do something (start sending notifications, stop sending notifications). One for each "function" (RX, TX), but if it's not configurable, then it doesn't need to exist. Standard value 0x2902 must be used.

The code below shows how the SERVICE_UUID can be searched and how the device may be connected with the connectGatt() function:

 
    private void processScanResult(ScanResult result) { 
        boolean foundDevice = false; 
 
        ScanRecord scanRecord = result.getScanRecord(); 
 
        if (scanRecord == null) { 
            return; 
        } 
 
        byte[] bytes = scanRecord.getServiceData(SERVICE_PARCEL_UUID); 
        if (bytes == null) { 
            List<ParcelUuid> uuids = scanRecord.getServiceUuids(); 
            if (uuids == null) 
                return; 
            if (uuids.isEmpty()) 
                return; 
            for (ParcelUuid uuid : uuids) { 
                Log.d(TAG, "Found scan record: " + uuid.toString()); 
                if (uuid.equals(SERVICE_PARCEL_UUID)) { 
                    Log.d(TAG, "Found proper scan record: " + uuid.toString()); 
                    foundDevice = true; 
                } 
            } 
            if (!foundDevice) 
                return; 
        } 
 
        Log.d(TAG, "Connecting to: " + scanRecord.toString()); 
        BluetoothDevice device = result.getDevice(); 
        String deviceAddress = device.getAddress(); 
         
        stopBleScan(); 
 
        BluetoothDevice mDevice = btAdapter.getRemoteDevice(deviceAddress); 
        mGatt = mDevice.connectGatt(this, false, gattCallback); 
    } 
 
    private ScanCallback leScanCallback = new ScanCallback() { 
        @Override 
        public void onBatchScanResults(List<ScanResult> results) { 
            for (ScanResult result : results) { 
                processScanResult(result); 
            } 
        } 
 
        @Override 
        public void onScanFailed(int errorCode) { 
            Log.d(TAG, "Scan error: " + errorCode); 
        } 
 
        @Override 
        public void onScanResult(int callbackType, ScanResult result) { 
            processScanResult(result); 
        } 
    };

UUIDs

The service (SERV_UUID) is a 128-bit UUID we're looking for and the characteristics is a UUID contained in the service itself. The CCCD is the Client Characteristics Configuration Descriptor, which is for configuring the notifications on/off for the service itself. It's a constant whereas the SERV_UUID and CHAR_UUID may vary - put whatever you wish into those but keep in mind that they'd better be aligned (similar to each other). All these are required for one way data / file transfer.

 
    private static final String SERV_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"; 
    private static final ParcelUuid SERVICE_PARCEL_UUID = ParcelUuid.fromString(SERV_UUID);

 
    private static final String CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"; 
    private static final UUID CHARACTERISTICS_UUID = UUID.fromString(CHAR_UUID);

 
    private static final UUID CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");

Why is the CCCD a 128 bit string? From the standard we see it being a 16 bit value 0x2902 only? Well, in like manner the Eddystone UUID is 0xFEAA, declared "0000FEAA-0000-1000-8000-00805F9B34FB". See that they differ only a bit (2902 -> FEAA). The rest just indicate the system that this is indeed a 16 bit UUID.


Starting to send and receive data

Below the gattCallback() is declared. Eventually we enable the notifications (see the line with comments) which results in the peripheral device to start transmitting data - 20 bytes at a time. Then we try to use the CONNECTION_PRIORITY_MAX in order to get the fastest connection parameters. This means faster data transfer if it's accepted by the peripheral device. It should enforce a connection parameter update request on the peripheral but if no support is present in the peripheral it's possible that the request is rejected (or not understood) and no faster parameters are used. Keep in mind that the peripheral device should be configured so that if the notifications are enabled then it will start sending data at 20 byte junks. In this example, only Android side is addressed.

 
    private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() { 
        @Override 
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { 
            switch (newState) { 
                case BluetoothProfile.STATE_CONNECTED: 
                    gatt.discoverServices(); 
                    break; 
                case BluetoothProfile.STATE_DISCONNECTED: 
                    gatt.close(); 
                    break; 
                default: 
            } 
 
        } 
 
        @Override 
        public void onServicesDiscovered(BluetoothGatt gatt, int status) { 
            if (gatt.getService(SERVICE_UUID) == null) { 
                gatt.disconnect(); 
                return; 
            } 
 
            if (gatt.getService(SERVICE_UUID).getCharacteristic(CHARACTERISTICS_UUID) == null) { 
                gatt.disconnect(); 
                return; 
            } 
 
            BluetoothGattCharacteristic characteristic = gatt 
                    .getService(SERVICE_UUID) 
                    .getCharacteristic(CHARACTERISTICS_UUID); 
 
            gatt.setCharacteristicNotification(characteristic, true); 
            BluetoothGattDescriptor descriptor = 
                    characteristic.getDescriptor(CCCD); 
 
            if (descriptor == null) { 
                gatt.disconnect(); 
                return; 
            } 
 
            // Enable the notifications 
            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); 
            gatt.writeDescriptor(descriptor); 
 
            if (!gatt.requestConnectionPriority(CONNECTION_PRIORITY_HIGH)) { 
                Log.i(TAG, "No CONNECTION_PRIORITY_HIGH!"); 
            } 
        } 
 
        @Override 
        public void onCharacteristicRead(BluetoothGatt gatt, 
                                         BluetoothGattCharacteristic 
                                                 characteristic, int status) { 
            readFromRemoteCharacteristics(characteristic); 
        } 
 
        @Override 
        public void onCharacteristicChanged(BluetoothGatt gatt, 
                                            BluetoothGattCharacteristic characteristic) { 
            readFromRemoteCharacteristics(characteristic); 
        } 
    };
 
 
    private void readFromRemoteCharacteristics(BluetoothGattCharacteristic 
                                                   characteristic) { 
        // Read from the correct UUID into a local file 
        if (CHARACTERISTICS_UUID.equals(characteristic.getUuid())) { 
            byte[] data = characteristic.getValue(); 
            dataRxAmount += data.length; 
            fileStoreData(data); 
        } 
    }

Eventually we store the received data to a local file. Often times there's only 20 bytes included in a notification.


Known bugs - easy to produce

gatt.disconnect() isn't enough if the whole BLE session needs to be terminated. If gatt.close() is not called at all, you may experience seeing duplicate data via two or more descriptors. For a clean termination, call disconnect() and close().


Data transfer issues

One might wonder if enabling the notifications from the remote device is enough. Do we need some flow control? No. Will some notifications be lost? In fact, no packets are lost and this should be the proper way of transferring data between two BLE devices. Of course, if there's only a few bytes to be exhanged this is not really the most optimal way. If you think there's a better way, I'll make the information available here, if your claim may be proven valid and you provide permissions for such.

The peripheral device should indicate somehow that the data transfer is over (the file has been transferred). Here we don't give any guidance on how to manage such signalling. Perhaps other-than-a-20-byte packet is sent finally containing magic numbers indicating the session is over - for example.

Why to use only 20 byte packets? The MTU may be changed, but to start with, use the 20 byte sizes before trying to optimize it in any way. There's an enormous number of devices that don't work with higher packet sizes. Don't try to get the most optimal data rates at the expense of compatibility at the early stages of your app. This is something for you to consider later on, though.

This was a brief solution to a known challenge in BLE. Data is sent via the notifications for achieving the best performance. I hope you find this article useful. Feedback is always welcome, first name dot last name at gmail.com

Sincerely, Eero Nurkkala, Offcode ltd. September the 25th, 2017.


Back to the introduction of SelfAd