Потоковая передача данных Bluetooth SPP в реальном времени на Android работает только в течение 5 секунд.

У меня есть самодельное устройство Bluetooth, измеряющее ЭКГ с частотой 500 Гц: каждые 2 мс устройство отправляет 9 байтов данных (заголовок, измерение ЭКГ, нижний колонтитул). Так что это примерно 9*500=4,5 Кбайт / с потока данных.

У меня есть программа на C++ для Windows, способная подключить устройство и получить поток данных (отображая его с помощью Qt/qwt). В этом случае я использую панель управления Windows для подключения устройства и подключаю его через виртуальный COM-порт, используя интерфейс boost serial_port. Это работает отлично, и я получаю свой поток данных в режиме реального времени: я получаю точку измерения каждые 2 мс или около того.

Я портировал всю программу C++ на Android через QtCreator (Qt 5.3.2). У меня были проблемы в реальном времени. Первые 5 секунд поток данных находился в режиме "реального времени", а затем производительность резко снижалась (см. " Как правильно выполнять потоковую передачу данных в режиме реального времени с использованием Java Android SDK").

Поскольку я думаю, что проблема может быть связана с C++/Qt, я написал совершенно чистый проект Java/Android с использованием Eclipse. И у него такая же проблема!!!

Вопросы: что-то не так с этим кодом? Почему я получаю данные в режиме реального времени всего за 5 первых секунд? Что происходит после 5 секунд интенсивного использования BT на платформе Android и почему это замедляет прием данных BT?

Вот моя программа на Java:

BluetoothHelper.java (с функциями для подключения / отключения / чтения и записи данных:

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
    }
}

основной файл Java, подключение и выполнение 10-секундного захвата:

    // 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();
    }

И вот, вывод программы:

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...

Как вы можете видеть, в течение первых 5 секунд буфер чтения остается слишком маленьким, и нет никакого момента, когда буфер пуст более 100 мс (см. Вывод кода "Нет данных ожидающих обработки"). Затем с пятой секунды мы:

  • начать иметь длительные периоды (~500 мс), когда буфер чтения остается пустым (InputStream::available() возвращает 0), даже если мое устройство постоянно отправляет данные в Android.
  • можно увидеть максимальный размер буфера значительно увеличивается.

После 5 первых секунд сбора данных создается впечатление, что данные где-то буферизируются и становятся доступными для чтения в InputStream блоками ~ 500 мс.....

Иногда это может быть даже хуже, данные не принимаются вообще через 5 секунд:

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!

Примечание: я пытался поспать несколько секунд, прежде чем создать BluetoothHelper и перед звонком startECG(), Такое же поведение (сбор данных замедляется или прекращается через 5 секунд).

Изменить: я испытываю это на:

  • Телефон Nexus 5, Android 4.4.2
  • Планшет Nexus 7, Android 4.4.2
  • Galaxy S4 с Android 4.4.2

Но не на Galaxy S3 с пользовательским CyanogenMod 11 Android 4.4.2: потоковая передача данных кажется идеальной, не происходит замораживание через 5 секунд, и данные поступают в режиме реального времени...

Изменить 15 декабря:

Как и предлагалось, перенесено чтение в отдельную ветку: сделано BluetoothHelper воплощать в жизнь Runnable и добавил эти методы / атрибуты в класс:

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;
        }
    }
}

Затем изменил абонента на:

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();

И это не решило проблему, вот вывод:

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

3 ответа

Решение

Эта проблема, по-видимому, похожа на ту, о которой сообщалось здесь.

Через 5 секунд у меня либо пропало соединение, либо поток в реальном времени резко замедлился.

Как уже говорилось, Android >4.3 явно не любит одностороннюю связь, превышающую 5 секунд. Так что теперь я отправляю на устройство фиктивную команду каждую 1 секунду (своего рода команда "keep-alive"), и теперь Android счастлив, потому что это больше не односторонняя связь... и поэтому поток данных так же хорош после пятая секунда, чем раньше!

Используйте концепцию потоков для одновременного чтения и записи байтов на периферийных устройствах. Используйте Android пример передачи данных Bluetooth для решения проблемы. Вы используете обычный Java-класс для отправки и получения данных на другое устройство, которое не подходит. Вы должны использовать концепцию Threading для отправки и получения данных через Bluetooth.

Пожалуйста, обратитесь к ссылке ниже, чтобы прочитать и записать данные через Bluetooth.

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

Вы не должны полагаться на InputStream.available() чтобы узнать, сколько байтов доступно в потоке (подробнее см. https://developer.android.com/reference/java/io/InputStream.html). Поскольку вы знаете точный размер вашего пакета данных (9 байт), каждый раз считывайте 9 байт в буфер: mInputStream.read(buffer, 0, 9),

С Bluetooth трудно гарантировать двоичную доставку в реальном времени, так как может быть много причин задержек (например, увеличение расстояния между устройствами, препятствия и т. Д.). Таким образом, обычно лучше постоянно звонить read и пересылать извлеченные части данных для обработки компонентов. Например, в одном из моих проектов я реализовал Android Service ожидание пакетов данных от Bluetooth и уведомление интерфейса пользователя с полученными данными. Вы можете реализовать Service или AsyncTask за это.

Еще одна рекомендация: избегайте ненужного выделения памяти в часто вызываемых методах (таких как readData). Вы можете измерить прошедшее время, используя System.currentTimeMillis(), Сборка мусора может быть одной из причин снижения производительности.

Другие вопросы по тегам