8

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
Community
  • 1
  • 1
jpo38
  • 20,821
  • 10
  • 70
  • 151
  • 1
    May be this will help: http://stackoverflow.com/questions/27274766/bluetooth-stream-stopped-after-updating-android-4-2-2-to-4-4-2 See answer. – Tupelo Honey Dec 29 '14 at 08:36
  • Can't believe it, this fixed my problem. Sending a dummy "keep-alive" command every second makes the connection and data streaming fully stable! Please post a real answer so that I can mark it as "good answer"! – jpo38 Jan 05 '15 at 10:27
  • You can add it as an (own) accepted answer, will be more visible such way – tomash Jan 16 '15 at 09:29
  • @tomash: I was expecting Tupelo Honey to do it (and then earn some reputation), but as he did not, and as you recommended, I did it. You are right it will make the solution more visible. – jpo38 Jan 16 '15 at 12:56

3 Answers3

6

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!

Community
  • 1
  • 1
jpo38
  • 20,821
  • 10
  • 70
  • 151
3

Use threading concept for simultaneously Read and Write Bytes on peripheral devices. Use android Bluetooth data transfer example to solve problem. You are using normal java class to send and receive data to another device that is not valid approach. You should use Threading concept to send and receive data over Bluetooth.

please refer below link to read and write data over Bluetooth.

http://developer.android.com/guide/topics/connectivity/bluetooth.html

kaushik parmar
  • 805
  • 6
  • 19
  • Thanks for the help, but, as I said to stacher, it does not fix the problem (see the edit at the end of my original post) – jpo38 Dec 15 '14 at 14:27
  • have you tried example that given on android developer link? – kaushik parmar Dec 15 '14 at 16:41
  • Absolutely, copy/pasted the code and started the thread with `new Thread( new ConnectedThread( mSocket )).start();`, added time tracking code in it and noticed that time between two `read` function returns moves from ~35ms to ~500ms after 5th second of acquisition. – jpo38 Dec 16 '14 at 07:50
1

You should not rely on InputStream.available() to tell how many bytes are available in the stream (see https://developer.android.com/reference/java/io/InputStream.html#available() for the details). Since you know the exact size of your data package (9 bytes), read 9 bytes each time into the buffer: mInputStream.read(buffer, 0, 9).

With Bluetooth it is hard to guarantee real-time binary delivery, as there might be many reasons for delays (e.g. increased distance between devices, obstacles, etc.). Thus, it is usually better to constantly call read and forward retrieved data portions to handling components. For instance, in one of my projects I implemented Android Service waiting for the data packages from Bluetooth and notifying UI with received data. You can implement a Service or an AsyncTask for this.

Another recommendation: avoid making unnecessary memory allocations in methods you call often (such as readData). You can measure elapsed time by using System.currentTimeMillis(). Garbage collection could be one of the reasons you experience degrading performance.

stealth
  • 371
  • 2
  • 8
  • Thanks for your proposal. I've just moved the code reading data to a while loop in a separate `Thread` and made sure no data is being allocated within the loop. I used blocking `read` function and outputed time elapsed between two returns of the `read` function. It did not fix the problem....during 5 secondes biggest time elapsed between two reads is ~40ms, after 5th second, it's between 500ms and almost 1000ms.... – jpo38 Dec 15 '14 at 14:17
  • I made an Edit to my original post to show the thread implementation and result... – jpo38 Dec 15 '14 at 14:26