Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Real-time Bluetooth SPP data streaming on Android only works for 5 seconds

I have a home-made bluetooth device measuring ECG at 500Hz: every 2 ms the device sends 9 bytes of data (header, ECG measurment, footer). So this is roughly a 9*500=4.5kbytes/s data stream.

I have a C++ Windows program able to connect the device and retrieve the data stream (displaying it with Qt/qwt). In this case, I use Windows control panel to bond the device and I connect it via a virtual COM port using boost serial_port interface. This works perfectly and I'm receiving my data stream in real time: I get a measurment point every 2ms or so.

I ported the whole C++ program on Android via QtCreator (Qt 5.3.2). I had real-time issues. Data stream was in "real-time" for the first 5 seconds, and then performance would dramatically slow down (see How to do good real-time data streaming using Java Android SDK).

Because I thougth the problem could be due to C++/Qt, I wrote a completely blank pure Java/Android project using Eclipse. And it has the same problem!!!

Questions are: Is there something wrong with this code? Why am I receiving data in real-time for only the 5 first seconds? What happens after 5 seconds of intensive BT usage on Android platform and why does it slow down the BT data reception?

Here is my Java program:

BluetoothHelper.java (with functions to connect/disconnect/read and write data:

package com.example.helloworld;

import android.util.Log;
import android.content.Context;
import android.os.Bundle;
import java.util.Locale;
import java.util.concurrent.Semaphore;
import java.lang.String;
import java.lang.Thread;
import java.io.IOException;
import java.io.OutputStream;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.lang.InterruptedException;
import android.app.Activity;
import android.app.AlertDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.bluetooth.BluetoothManager;
import android.util.SparseArray;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.BroadcastReceiver;
import java.util.UUID;
import java.util.Date;
import java.util.Calendar;
import java.util.Vector;
import java.util.Set;
import java.util.Arrays;

public class BluetoothHelper
{
    private BluetoothManager mBluetoothManager;
    private BluetoothAdapter mBluetoothAdapter;
    private BluetoothDevice mDevice;
    private BluetoothSocket mSocket;
    private OutputStream mOutputStream;
    private InputStream mInputStream;
    private BroadcastReceiver mReceiver;
    private Activity myActivity;
    private Vector<BluetoothDevice> mDevices;
    private byte[] mHeader;
    private byte[] mFrame;

    public BluetoothHelper(Activity a)
    {
        myActivity = a;
        mHeader = new byte[3];
        mFrame = new byte[256];
        mDevices = new Vector();
    }

    /* Check bluetooth is enabled, return "" if OK, else, return error string */
    public String initializeBluetooth(){

        String error = "";
        System.out.println("Initializing bluetooth...");

        mBluetoothManager = (BluetoothManager) myActivity.getSystemService(Context.BLUETOOTH_SERVICE);
        if ( mBluetoothManager == null )
        {
            error = "Bluetooth manager is not found";
        }
        else
        {
            mBluetoothAdapter = mBluetoothManager.getAdapter();
            if( mBluetoothAdapter == null )
            {
                error = "Bluetooth adapter is not found";
            }
            else if( ! mBluetoothAdapter.isEnabled() )
            {
                error = "Bluetooth adapter is off";
            }
            else
            {
                System.out.println("Bluetooth successfully initialized");
                return "";
            }
        }

        return error;
    }

    private void addDevice( final BluetoothDevice device )
    {
        mDevices.add(device);
    }

    public Vector<BluetoothDevice> getDevices() { return mDevices; }

    /* Clear previously detected device list */
    public boolean clearDeviceList(){
        // Clear old list
        mDevices.clear();
        return true;
    }

    /* Fill local device list with paired devices */
    public boolean addPairedDevices(){
        //System.out.println("Entering addPairedDevices");

        if( mBluetoothAdapter == null )
        {
            System.out.println("No bluetooth adapter");
            return false;
        }

        Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
        // If there are paired devices
        if (pairedDevices.size() > 0)
        {
            //System.out.println("Found paired devices");
            // Loop through paired devices
            for (BluetoothDevice device : pairedDevices)
            {
                addDevice( device );
            }
        }

        return true;
    }

    public String connectToDevice(final BluetoothDevice device)
    {
        if ( mDevice != null )
            disconnectDevice();

        if( mBluetoothAdapter == null || myActivity == null )
            return "System not initialized or bluetooth not active";

        if ( device.getBondState() != BluetoothDevice.BOND_BONDED )
        {
            // TODO: find a way to do a synchronized bounding operation
            return "Device is not bonded";
        }

        final boolean[] the_result = new boolean[1];
        the_result[0] = false;

        final Semaphore mutex = new Semaphore(0);

        Runnable connectRunnable = new Runnable() {
                @Override
                public void run()                {

                    UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");

                   try
                   {
                        mSocket = device.createInsecureRfcommSocketToServiceRecord( MY_UUID );
                        System.out.println("Created RFcomm socket");
                        mSocket.connect();
                        if ( mSocket.isConnected() )
                        {
                            System.out.println("Connected RFcomm socket");
                            mOutputStream = mSocket.getOutputStream();
                            mInputStream = mSocket.getInputStream();
                            System.out.println("Retrieved output stream");
                            the_result[0] = true;
                        }
                        else
                        {
                            System.out.println("Failed to connect RFcomm socket");
                        }
                   }
                   catch (IOException e)
                   {
                        System.out.println("Failed to open RFcomm socket (createRfcommSocketToServiceRecord)");
                        System.out.println(e.toString());
                   }

                   mutex.release();
                }
            };

        myActivity.runOnUiThread( connectRunnable );

        // waiting for thread to be completed...
        try {
            mutex.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        if ( the_result[0] )
        {
            System.out.println("Connection succeeded");
            return "";
        }
        else
        {
            System.out.println("Connection failed");
            return "Failed to connect device";
        }
    }

    /* Request to disconnect the device */
    public boolean disconnectDevice(){

        System.out.println("Disconnecting device...");

        if ( mSocket != null )
        {
            // block read/write
            mOutputStream = null;
            mInputStream = null;

            try
            {
                mSocket.close();
            }
            catch( IOException e )
            {
                e.printStackTrace();
                return false;
            }
            mSocket = null;
        }

        mDevice = null;

        return true;
    }

    /* Send bytes to the connected device */
    public boolean writeData( byte[] buffer )
    {
        if( mOutputStream == null )
        {
            System.out.println("No connection, can't send data");
        }
        else
        {
            try
            {
                mOutputStream.write( buffer );
                return true;
            }
            catch (IOException e)
            {
                System.out.println( "Failed to send data" );
                e.printStackTrace();
            }
        }
        return false;
    }

    public static String byteArrayToHex(byte[] a, int size) {
        StringBuilder sb = new StringBuilder(size * 5);
        for( int i = 0; i != size; ++i )
           sb.append(String.format("0x%02x ", a[i] & 0xff));
        return sb.toString();
    }

    public int getBytesPending()
    { 
        try
        {
            return mInputStream.available();
        }
        catch (IOException e)
        {
            return 0;
        }
    }

    /* Non blocking read function. Read bytes from the connected device.
     * Return number of bytes read
     * return 0 if not enough bytes available
     * return -1 in case of error
     */
    public int readData( byte[] buffer, int size, boolean blocking )
    {
        if ( mInputStream == null )
        {
            System.out.println("No connection, can't receive data");
        }
        else
        {
            try
            {
                final boolean verbose = false;

                if ( blocking )
                {
                    if ( verbose )
                        System.out.println( "Blocking request of " + buffer.length + " byte(s)" );    
                    int res = 0;
                    int temp = 0;
                    while ( true )
                    {
                        temp = mInputStream.read( buffer, res, size - res );

                        res += temp;

                        if ( res >= size )
                        {
                            break;
                        }
                        else
                        {
                            if ( verbose )
                                System.out.println( "Received " + res + " byte(s) to far : " + byteArrayToHex(buffer,size) );
                        }

                        try {
                            Thread.sleep(10);
                        } catch(InterruptedException ex) {

                        }
                    }
                    if ( verbose )
                        System.out.println( "Received " + res + " byte(s) : " + byteArrayToHex(buffer,size) );
                    return res;
                }
                else
                {
                    int available = mInputStream.available();

                    if ( verbose && available != 0 )
                    {
                        Calendar c = Calendar.getInstance();
                        Date date = new Date();
                        c.setTime(date);
                        c.get(Calendar.MILLISECOND);

                        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                        String currentTime = sdf.format(date);

                        System.out.println( currentTime + ":" + c.get(Calendar.MILLISECOND) + " - " + available + " bytes available, requested " + buffer.length );
                    }

                    if ( available >= size )
                    {
                        int res = mInputStream.read( buffer, 0, size ); // only call read if we know it's not blocking
                        if ( verbose )
                            System.out.println( "Received " + res + " byte(s) : " + byteArrayToHex(buffer,size) );
                        return res;
                    }
                    else
                    {
                        return 0;
                    }
                }
            }
            catch (IOException e)
            {
                System.out.println( "Failed to read data...disconnected?" );
                //e.printStackTrace();
            }
        }
        return -1;
    }

    public byte[] readNextFrame( boolean blocking )
    {
        if ( readData( mHeader, mHeader.length, blocking ) == mHeader.length )
        {
            int size = mHeader[2];
            if ( size < 0 )
                size = -size;

            if ( readData( mFrame, size, blocking ) == size )
            {
                byte[] res = new byte[mHeader.length + size];
                System.arraycopy(mHeader, 0, res, 0, mHeader.length);
                System.arraycopy(mFrame, 0, res, mHeader.length, size);
                return res;
            }
        }

        return null;        
    }

    */ read frame but without allocating any memory, does not retur condumed bytes */
    public boolean eatNextFrame( boolean blocking )
    {
        if ( readData( mHeader, mHeader.length, blocking ) == mHeader.length )
        {
            int size = mHeader[2];
            if ( size < 0 )
                size = -size;

            if ( readData( mFrame, size, blocking ) == size )
            {
                return true;
            }
        }

        return false;       
    }

    public boolean startECG()
    {
        // some code sending instructions to configure my device
    }
}

main Java file, connecting and doing a 10sec acquisition:

    // Here is the code for Medoc:
    BluetoothHelper helper = new BluetoothHelper(this);
    String error = helper.initializeBluetooth();
    if ( error.isEmpty() )
    {
        if ( helper.addPairedDevices( ) )
        {
            if ( !helper.getDevices().isEmpty() )
            {
                if ( helper.getDevices().size() == 1 )
                {
                    BluetoothDevice device = helper.getDevices().firstElement();

                    error = helper.connectToDevice( device );

                    if ( error.isEmpty() )
                    {
                        if ( helper.startECG() )
                        {
                            // acquiere data for 10 seconds
                            Date start = new Date();
                            Date end = new Date();
                            Date empty = null;
                            int lastMinute = 0;
                            int maxBufferSize = 0;
                            boolean receivedData = false;
                            while ( end.getTime() - start.getTime() < 10 * 1000 )
                            {
                                int currentMinute = (int) (( end.getTime() - start.getTime() ) / 1000);
                                if ( currentMinute != lastMinute )
                                {
                                    if ( receivedData )
                                        System.out.println( "During second #" + lastMinute + " max buffer size was : " + maxBufferSize );
                                    else
                                        System.out.println( "During second #" + lastMinute + " no data was received!" );
                                    maxBufferSize = 0;
                                    receivedData = false;
                                    lastMinute = currentMinute;
                                }

                                if ( helper.eatNextFrame(false) )
                                {
                                    receivedData = true;
                                }
                                if ( helper.getBytesPending() == 0 )
                                {
                                    if ( empty == null )
                                    {
                                        empty = new Date();
                                    }
                                }
                                else
                                {
                                    if ( empty != null )
                                    {
                                        Date now = new Date();
                                        int elapsed = (int) ( now.getTime() - empty.getTime() );
                                        if ( elapsed > 100 )
                                            System.out.println( "No pending data, during " + elapsed + "ms" );
                                        empty = null;                                                   
                                    }
                                }

                                maxBufferSize = Math.max( helper.getBytesPending(), maxBufferSize );

                                end = new Date();
                            }

                            AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this);
                            dlgAlert.setMessage( "Done" );
                            dlgAlert.setPositiveButton("Ok",null);
                            dlgAlert.create().show();
                        }
                        else
                        {
                            error = "Failed to start ECG";
                        }

                        helper.disconnectDevice();
                    }
                }
                else
                {
                    error = "Too many devices found";
                }
            }
            else
            {
                error = "No device found";
            }
        }
        else
        {
            error = "Failed to scan for devices";
        }
    }

    if ( !error.isEmpty() )
    {
        AlertDialog.Builder dlgAlert2 = new AlertDialog.Builder(this);
        dlgAlert2.setMessage( error );
        dlgAlert2.setPositiveButton("Ok",null);
        dlgAlert2.create().show();
    }

And here, the output of the program:

12-01 14:12:51.755: I/System.out(15940): During second #0 max buffer size was : 63
12-01 14:12:52.755: I/System.out(15940): During second #1 max buffer size was : 133
12-01 14:12:53.755: I/System.out(15940): During second #2 max buffer size was : 66
12-01 14:12:54.755: I/System.out(15940): During second #3 max buffer size was : 61
12-01 14:12:55.755: I/System.out(15940): During second #4 max buffer size was : 129
12-01 14:12:56.705: I/System.out(15940): No pending data, during 501ms
12-01 14:12:56.755: I/System.out(15940): During second #5 max buffer size was : 939
12-01 14:12:57.755: I/System.out(15940): During second #6 max buffer size was : 980
12-01 14:12:58.755: I/System.out(15940): During second #7 max buffer size was : 1008
12-01 14:12:59.195: I/System.out(15940): No pending data, during 488ms
12-01 14:12:59.695: I/System.out(15940): No pending data, during 489ms
12-01 14:12:59.755: I/System.out(15940): During second #8 max buffer size was : 990
12-01 14:13:00.185: I/System.out(15940): No pending data, during 490ms
12-01 14:13:01.205: I/System.out(15940): Disconnecting device...

As you can see, during the 5 first seconds, read buffer remains prettry small and there is no moment when buffer is empty for more than 100ms (see code outputing "No pending data"). Then, from the fifth second we :

  • start having long periods (~500ms) where the read buffer remains empty (InputStream::available() returns 0) even if my device is permanently sending data to Android.
  • can see the buffer max size grows significantly.

After the 5 first seconds of data acquisition, it's as if data are getting bufferized somewhere and are made available for reading in the InputStream by blocks of ~500ms.....

Sometimes, it could be even worst, there is no data being received at all after 5sec:

12-01 14:35:54.595: I/System.out(16386): During second #0 max buffer size was : 22
12-01 14:35:55.595: I/System.out(16386): During second #1 max buffer size was : 93
12-01 14:35:56.595: I/System.out(16386): During second #2 max buffer size was : 108
12-01 14:35:57.595: I/System.out(16386): During second #3 max buffer size was : 61
12-01 14:35:58.595: I/System.out(16386): During second #4 max buffer size was : 64
12-01 14:35:59.595: I/System.out(16386): During second #5 max buffer size was : 63
12-01 14:36:00.595: I/System.out(16386): During second #6 no data was received!
12-01 14:36:01.595: I/System.out(16386): During second #7 no data was received!
12-01 14:36:02.595: I/System.out(16386): During second #8 no data was received!

Note: I tried to sleep some seconds before creating BluetoothHelper and before calling startECG(). Same behaviour (acquisition slows down or stops after 5 seconds).

Edit: I'm experiencing that on:

  • Nexus 5 phone, Android 4.4.2
  • Nexus 7 tablet, Android 4.4.2
  • Galaxy S4 with Android 4.4.2

But not on a Galaxy S3 with custom CyanogenMod 11 Android 4.4.2: data streaming seems perfect, no freezing after 5sec and data are arriving in real-time...

Edit December 15th:

As proposed, moved read to a separate thread: Made BluetoothHelper implement Runnable and added those methods/attributes to the class:

private int mFramesReceived;
private long mLongestPause;
public void clearReceived()
{
    mFramesReceived = 0;
    mLongestPause = 0;
}

public int received()
{
    return mFramesReceived;
}

public long longestPause()
{
    return mLongestPause;
}

@Override
public void run() {

    System.out.println( "Started thread" );

    int lastSeconde = 0;
    long currentTimeMillis = System.currentTimeMillis();
    long started = System.currentTimeMillis();

    // Keep listening to the InputStream until an exception occurs
    while (true) {
        if ( eatNextFrame( true ) )
        {
            //System.out.println( "Got some data" );
            mLongestPause = Math.max( mLongestPause, System.currentTimeMillis() - currentTimeMillis );
            currentTimeMillis = System.currentTimeMillis();
            mFramesReceived++;

            int currentSeconde = (int) (( System.currentTimeMillis() - started ) / 1000);
            if ( currentSeconde != lastSeconde )
            {
                if ( mFramesReceived != 0 )
                    System.out.println( "During second #" + lastSeconde + " max pause was : " + mLongestPause );
                else
                    System.out.println( "During second #" + lastSeconde + " no data was received!" );

                clearReceived();

                lastSeconde = currentSeconde;
            }
        }
        else
        {
            System.out.println( "Failed to get some data, connection closed?" );
            break;
        }
    }
}

Then changed caller to:

if ( helper.startECG() )
{
    new Thread(helper).start();

    try {
        Thread.sleep(10000); // wait 10 seconds
    } catch(InterruptedException ex) {
        Thread.currentThread().interrupt();
    }

    AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this);
    dlgAlert.setMessage( "Done" );
    dlgAlert.setPositiveButton("Ok",null);
    dlgAlert.create().show();
}
else
{
    error = "Failed to start ECG";
}

helper.disconnectDevice();

And it did not fix the problem, here is the output:

During second #0 max pause was : 48
During second #1 max pause was : 45
During second #2 max pause was : 33
During second #3 max pause was : 35
During second #4 max pause was : 58
During second #5 max pause was : 498
During second #6 max pause was : 477
During second #7 max pause was : 480
During second #8 max pause was : 986
During second #9 max pause was : 497
like image 402
jpo38 Avatar asked Dec 01 '14 13:12

jpo38


1 Answers

This problem is apparently similar to the one reported here.

After 5 seconds, I had either a connection lost, either real-time streaming being dramatically slow down.

As said here Android >4.3 apparently does not like one-way communication exceeding 5 secondes. So I'm now sending a dummy command to the device every 1 seconde (kind of "keep-alive" command) and now Android is happy because it's not a one-way communication anymore...and so data streaming is as good after the fifth second than before!

like image 63
jpo38 Avatar answered Oct 20 '22 00:10

jpo38