Android蓝牙健康设备开发:Health Device Profile(HDP)

最近按导师的要求做了个小项目:用Android手机跟蓝牙血压计通信。在网上查了很多资料,发现有许多文章是讲解Android蓝牙开发,但是在中文社区缺少针对实现HDP profile的蓝牙健康设备的文章,所以整理了相关知识和代码做一个总结。做了一点微小的贡献,谢谢大家。
1、相关概念
HDP:Health Device Profile顾名思义是针对蓝牙健康设备(如蓝牙血压计、蓝牙体重秤)的一个profile,由蓝牙技术联盟(Bluetooth SIG)发布。并不是所有蓝牙设备都可以采用HDP,只有经典蓝牙(BR/EDR)设备才可以。低功耗蓝牙设备,即蓝牙4.0及以上,采用的是GATT等profile。

Android蓝牙健康设备开发:Health Device Profile(HDP)_第1张图片

上图是HDP的协议栈。在通信时,蓝牙健康设备作为source,Android手机作为sink。图中L2CAP、SDP、MCAP是蓝牙通信的底层协议,中间是IEEE11073协议。11073是IEEE发布的一个健康设备的协议簇,其下包含了许多不同种类的健康设备协议,如11073-10407是血压计的协议。在数据传输时,手机与健康设备根据此协议来发送请求、解析数据等。最上层的application在本文中自然指的就是Android app。
Android蓝牙模块:android.bluetooth是蓝牙的开发包,里面包含了所有蓝牙相关的类,无论是经典蓝牙还是低功耗蓝牙。其中,本文重点关注的是BluetoothHealth这个类,还用到了一些蓝牙基础类BluetoothAdapter BluetoothDevice等,在这里就不展开介绍了。BluetoothHealth是在API14时引入的,所以系统版本高于14的Android手机都可以与采用HDP的健康设备通信。BluetoothHealth中包括了与HDP设备建立通信的API,具体请参见官方文档。在官方文档中有一个建立通信的流程:
1、调用[getProfileProxy(Context, BluetoothProfile.ServiceListener, int)](https://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#getProfileProxy(android.content.Context, android.bluetooth.BluetoothProfile.ServiceListener, int))来获取代理对象的连接。
2、创建BluetoothHealthCallback回调,调用[registerSinkAppConfiguration(String, int, BluetoothHealthCallback)](https://developer.android.com/reference/android/bluetooth/BluetoothHealth.html#registerSinkAppConfiguration(java.lang.String, int, android.bluetooth.BluetoothHealthCallback))注册一个sink端的应用程序配置。
3、将手机与健康设备配对,这一步一般在手机的“设置”中完成。
4、使用[connectChannelToSource(BluetoothDevice, BluetoothHealthAppConfiguration)](https://developer.android.com/reference/android/bluetooth/BluetoothHealth.html#connectChannelToSource(android.bluetooth.BluetoothDevice, android.bluetooth.BluetoothHealthAppConfiguration))来建立一个与健康设备的通信channel。有的设备会自动建立通信,不需要在代码中调用这个方法。第二步中的回调会指示channel的状态变化。
5、用ParcelFileDescriptor来读取健康设备传来的数据,并根据IEEE 11073来解析数据。
6、通信结束后,关闭通信channel,注销应用程序配置。
看完这个流程是不是一脸懵逼?不要慌,这里官方文档实在太抽象了,必须要配合实际的代码才能看懂。
2、demo代码
项目的目标是与经典蓝牙血压计进行通信,获取血压计测量的数值和日期。这里血压计的型号是A&D UA-767PBT-C。项目中的代码是以github上这个项目为基础,根据需求进行修改而实现的。

Android蓝牙健康设备开发:Health Device Profile(HDP)_第2张图片

项目结构

上图是项目的结构,非常简单,只有一个activity和一个service。activity与用户交互,service绑定在activity中与蓝牙血压计建立通信、交换数据。
首先AndroidManifest.xml中注册蓝牙权限:



MainActivity

主要作用是显示血压计传过来的数据,以及绑定服务。
activity的布局如下:

Android蓝牙健康设备开发:Health Device Profile(HDP)_第3张图片
demo.png

上半部分是一个ArrayList,用于显示手机所配对的蓝牙设备的名称和Mac地址。下半部分会显示一次血压测量的参数:收缩压、舒张压、心率、测量时间。

在MainActivity中一些重要私有变量:

private static final int REQUEST_ENABLE_BT = 1;  //用于打开手机蓝牙
private static final int HEALTH_PROFILE_SOURCE_DATA_TYPE = 0x1007;  //IEEE 11073中规定的血压数据类型
private BluetoothAdapter mBluetoothAdapter;
private Messenger mHealthService;  //用于与service通信
private boolean mHealthServiceBound; //用于判断service是否与此activity绑定

打开蓝牙:


       if (!mBluetoothAdapter.isEnabled()) {
            Intent enableIntent = new Intent(
                    BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
        } else {
            initialize();
        }
 

启动服务:

            // Sets up communication with HDPService.
            private ServiceConnection mConnection = new ServiceConnection() {
                public void onServiceConnected(ComponentName name, IBinder service) {
                    mHealthServiceBound = true;
                    Message msg = Message.obtain(null,
                            HDPService.MSG_REG_CLIENT);
                    msg.replyTo = mMessenger;
                    mHealthService = new Messenger(service);
                    try {
                        mHealthService.send(msg);
                        //register blood pressure data type
                        sendMessage(HDPService.MSG_REG_HEALTH_APP,
                                HEALTH_PROFILE_SOURCE_DATA_TYPE);
                    } catch (RemoteException e) {
                        Log.w(TAG, "Unable to register client to service.");
                        e.printStackTrace();
                    }
                }
    
                public void onServiceDisconnected(ComponentName name) {
                    mHealthService = null;
                    mHealthServiceBound = false;
                }
            };
    private void initialize() {
        // Starts health service.
        Intent intent = new Intent(this, HDPService.class);
        //startService(intent);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

用handler和message来实现activity与service的通信:

    private Handler mIncomingHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
              ......
            }
        }
    };

    private final Messenger mMessenger = new Messenger(mIncomingHandler);

HDPService

这个service是核心部分,手机与蓝牙血压计的通信就是在此实现的。
service中部分重要私有变量:

    private BluetoothHealthAppConfiguration mHealthAppConfig; //BluetoothHealthAppConfiguration
    private BluetoothAdapter mBluetoothAdapter;  //BluetoothAdapter
    private BluetoothHealth mBluetoothHealth;  //代理对象

在service中按照官方文档的流程一步一步实现与蓝牙血压计的通信。
1、获取代理对象:

    public void onCreate() {
        super.onCreate();
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
            // Bluetooth adapter isn't available.  The client of the service is supposed to
            // verify that it is available and activate before invoking this service.
            stopSelf();
            return;
        }
        if (!mBluetoothAdapter.getProfileProxy(this, mBluetoothServiceListener,
                BluetoothProfile.HEALTH)) {
            Toast.makeText(this, "HDP not available",
                    Toast.LENGTH_LONG).show();
            stopSelf();
            return;
        }
    }

代码中的mBluetoothServiceListener是一个回调,如果getProfileProxy方法返回为真,就进入回调。在回调中获取了代理对象属于BluetoothHealth类的proxy。

private final BluetoothProfile.ServiceListener mBluetoothServiceListener =
            new BluetoothProfile.ServiceListener() {
                public void onServiceConnected(int profile, BluetoothProfile proxy) {
                    if (profile == BluetoothProfile.HEALTH) {
                        mBluetoothHealth = (BluetoothHealth) proxy;
                        Log.d(TAG, "onServiceConnected to profile: " + profile);
                    }
                }

                public void onServiceDisconnected(int profile) {
                    if (profile == BluetoothProfile.HEALTH) {
                        mBluetoothHealth = null;
                    }
                    Log.d(TAG, "onServiceDisconnected");
                }
            };

2、注册BluetoothHealthAppConfiguration:

mBluetoothHealth.registerSinkAppConfiguration(TAG, dataType, mHealthCallback);
private final BluetoothHealthCallback mHealthCallback = new BluetoothHealthCallback() {
        // Callback to handle application registration and unregistration events.  The service
        // passes the status back to the UI client.
        public void onHealthAppConfigurationStatusChange(BluetoothHealthAppConfiguration config,
                                                         int status) {
            if (status == BluetoothHealth.APP_CONFIG_REGISTRATION_FAILURE) {
                mHealthAppConfig = null;
                sendMessage(STATUS_HEALTH_APP_REG, RESULT_FAIL, null);
                Log.d(TAG, "APP_CONFIG_REGISTRATION_FAILURE");
            } else if (status == BluetoothHealth.APP_CONFIG_REGISTRATION_SUCCESS) {
                mHealthAppConfig = config;
                sendMessage(STATUS_HEALTH_APP_REG, RESULT_OK, null);
                Log.d(TAG, "APP_CONFIG_REGISTRATION_SUCCESS");
            } else if (status == BluetoothHealth.APP_CONFIG_UNREGISTRATION_FAILURE ||
                    status == BluetoothHealth.APP_CONFIG_UNREGISTRATION_SUCCESS) {
                sendMessage(STATUS_HEALTH_APP_UNREG,
                        status == BluetoothHealth.APP_CONFIG_UNREGISTRATION_SUCCESS ?
                                RESULT_OK : RESULT_FAIL, null);
                Log.d(TAG, "UNREGISTRATION");
            }
        }

        // Callback to handle channel connection state changes.
        // Note that the logic of the state machine may need to be modified based on the HDP device.
        // When the HDP device is connected, the received file descriptor is passed to the
        // ReadThread to read the content.
        public void onHealthChannelStateChange(BluetoothHealthAppConfiguration config,
                                               BluetoothDevice device, int prevState, int newState, ParcelFileDescriptor fd,
                                               int channelId) {
//            if (Log.isLoggable(TAG, Log.DEBUG))
            Log.d(TAG, String.format("prevState\t%d ----------> newState\t%d",
                    prevState, newState));
            if (prevState == BluetoothHealth.STATE_CHANNEL_CONNECTING &&
                    newState == BluetoothHealth.STATE_CHANNEL_CONNECTED) {
                if (config.equals(mHealthAppConfig)) {
                    mChannelId = channelId;
                    sendMessage(STATUS_CREATE_CHANNEL, RESULT_OK, null);
                    (new ReadThread(fd)).start();
                } else {
                    sendMessage(STATUS_CREATE_CHANNEL, RESULT_FAIL, null);
                }
            } else if (prevState == BluetoothHealth.STATE_CHANNEL_CONNECTING &&
                    newState == BluetoothHealth.STATE_CHANNEL_DISCONNECTED) {
                sendMessage(STATUS_CREATE_CHANNEL, RESULT_FAIL, null);
            } else if (newState == BluetoothHealth.STATE_CHANNEL_DISCONNECTED) {
                Log.d(TAG, "I'm in State Channel Disconnected.");
                if (config.equals(mHealthAppConfig)) {
                    sendMessage(STATUS_DESTROY_CHANNEL, RESULT_OK, null);

                } else {
                    sendMessage(STATUS_DESTROY_CHANNEL, RESULT_FAIL, null);
                }
            } else if (prevState == BluetoothHealth.STATE_CHANNEL_DISCONNECTED &&
                    newState == BluetoothHealth.STATE_CHANNEL_CONNECTED) {
                if (config.equals(mHealthAppConfig)) {
                    mChannelId = channelId;
                    sendMessage(STATUS_CREATE_CHANNEL, RESULT_OK, null);
                    (new ReadThread(fd)).start();
                } else {
                    sendMessage(STATUS_CREATE_CHANNEL, RESULT_FAIL, null);
                }
            }
        }
    };

mHealthCallback是其中的关键,有两个方法onHealthAppConfigurationStatusChange和onHealthChannelStateChange。
onHealthAppConfigurationStatusChange回调会监听AppConfiguration的状态,会在registerSinkAppConfiguration函数返回后调用。
onHealthChannelStateChange回调会监听与source端(在这里也就是血压计)通信通道的状态变化,如果收到发来的数据,会接收到一个ParcelFileDescriptor用于读取。

3、将手机与健康设备配对
这一步在手机的系统设置中进行,不必严格按照这个流程的顺序,可以事先配对好。

4、建立通信通道
这一步不是所有设备都需要,部分设备会自动建立通道与sink端(手机)进行通信。我们所用的这个蓝牙血压计就是如此,会自动建立通道。
如果不能自动建立通信,可以主动调用如下代码:

mBluetoothHealth.connectChannelToSource(mDevice, mHealthAppConfig);

mDevice是将要建立通信的source端,可以用mBluetoothAdapter获取与手机配对的任一设备。

5、读取、解析数据
在与蓝牙设备通信时,我们需要知道一点:通信是双向的,设备会向手机发信息,手机可以也需要向设备发信息。在service中建了两个线程,一个用于读数据,一个用于写数据,通过这两个线程实现与设备的数据交换。

首先看读线程

    private class ReadThread extends Thread {
        private ParcelFileDescriptor mFd;

        public ReadThread(ParcelFileDescriptor fd) {
            super();
            mFd = fd;
            Log.i(TAG, "read thread");
        }


        @Override
        public void run() {
          ...
        }
    }

读线程的调用是在回调方法onHealthChannelStateChange中的,当判断通信通道打开时,会开启一个读线程,并且将回调中提供的ParcelFileDescriptor传入读线程的构造方法,用于读取数据。
具体的读取和解析过程我们来看run方法:

FileInputStream fis = new FileInputStream(mFd.getFileDescriptor());
            byte data[] = new byte[300]; //假定有300byte的数据
            try {
                while (fis.read(data) > -1) {
                    if (data[0] != (byte) 0x00) {
                        String test = byte2hex(data);
                        Log.i(TAG, "test: " + test);
                        if (data[0] == (byte) 0xE2) {
                            Log.i(TAG, "E2");
                            //data_AR
                            count = 1;
                            (new WriteThread(mFd)).start();
                            try {
                                sleep(100);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            count = 2;
                            (new WriteThread(mFd)).start();
                        } else if (data[0] == (byte) 0xE7) {
                            Log.i(TAG, "E7");
                            if (data[18] == (byte) 0x0d && data[19] == (byte) 0x1d)  //fixed report
                            {
                                count = 3;
                                //set invoke id so get correct response
                                invoke = new byte[]{data[6], data[7]};
                                //write back response
                                (new WriteThread(mFd)).start();
                                //parse data!!

                                int length = data[21];
                                Log.i(TAG, "length is " + length);
                                int number_of_data_packets = data[22 + 5]; //03
                                //packet_start starts from handle 0 byte
                                int packet_start = 30; 
                                final int SYS_DIA_MAP_DATA = 1;
                                final int PULSE_DATA = 2;
                                final int ERROR_CODE_DATA = 3;
                                for (int i = 0; i < number_of_data_packets; i++) {
                                    int obj_handle = data[packet_start + 1]; 
                                    switch (obj_handle) {
                                        case SYS_DIA_MAP_DATA:
                                            int sys = byteToUnsignedInt(data[packet_start + 9]);
                                            int dia = byteToUnsignedInt(data[packet_start + 11]);
                                            int map = byteToUnsignedInt(data[packet_start + 13]);
                                            //create team string... 9+13~9+20
                                            Log.i(TAG, "sys is " + sys);
                                            sendMessage(RECEIVED_SYS, sys, null);
                                            Log.i(TAG, "dia is " + dia);
                                            sendMessage(RECEIVED_DIA, dia, null);
                                            Log.i(TAG, "map is " + map);
                                            break;
                                        case PULSE_DATA:
                                            //parse
                                            int pulse = byteToUnsignedInt(data[packet_start + 5]);
                                            Log.i(TAG, "pulse is " + pulse);
                                            sendMessage(RECEIVED_PUL, pulse, null);
                                            String month = byteToString(data[packet_start + 8]);
                                            String day = byteToString(data[packet_start + 9]);
                                            String year = byteToString(data[packet_start + 6]) + byteToString(data[packet_start + 7]);
                                            String hour = byteToString(data[packet_start + 10]);
                                            String minute = byteToString(data[packet_start + 11]);
                                            String date = year + "." + month + "." + day + " " + hour + ":" + minute;
                                            Log.v(TAG, "the date is " + date);
                                            sendMessage(RECEIVED_DATE, 0, date);
                                            break;
                                        case ERROR_CODE_DATA:
                                            //need more signal
                                            break;
                                    }
                                    packet_start += 4 + data[packet_start + 3];    //4 = ignore beginning four bytes
                                }
                            } else {
                                count = 2;
                            }
                        } else if (data[0] == (byte) 0xE4) {
                            count = 4;
                            (new WriteThread(mFd)).start();
                        }
                        //zero out the data
                        for (int i = 0; i < data.length; i++) {
                            data[i] = (byte) 0x00;
                        }
                    }
                    sendMessage(STATUS_READ_DATA, 0, null);
                }
            } catch (IOException ioe) {
            }
            if (mFd != null) {
                try {
                    mFd.close();
                } catch (IOException e) { /* Do nothing. */ }
            }
            sendMessage(STATUS_READ_DATA_DONE, 0, null);
        }

while循环中是对读取数据的解析,这里的解析必须参考IEEE 11073中的内容。这个demo用到了血压计,因此就要使用IEEE 11073-10407(专门针对血压计的协议)。使用其他类型的设备也就要使用相应的IEEE 11073-xxxxx协议。
回到demo中的数据读取和解析,这是一个根据IEEE 11073-10407进行手机与蓝牙设备互相发送数据的过程,下图是运行的log:

Android蓝牙健康设备开发:Health Device Profile(HDP)_第4张图片
log.png

大致解释一下流程:
按下测量按钮后,蓝牙血压计发来一个e2开头的数据,这是一个请求连接的信号。
收到请求连接后,手机用写线程发送给血压计一个回复同意连接,接着再发送一个数据请求血压计的特征。
血压计发来一个e7开头的数据,回复血压计的特征。
测量完毕后,血压计发来一个e7开头的数据,是这次测量的报告。
手机回复确认收到测量数据。
血压计发来一个e4开头的数据,请求断开连接。
手机回复确认断开连接。

想要查看详细数据请看IEEE 11073-10407!请看IEEE 11073-10407!请看IEEE 11073-10407!

接下去是写线程:

private class WriteThread extends Thread {
        private ParcelFileDescriptor mFd;

        public WriteThread(ParcelFileDescriptor fd) {
            super();
            mFd = fd;
        }

        @Override
        public void run() {
            FileOutputStream fos = new FileOutputStream(mFd.getFileDescriptor());
//          Association Response [0xE300]
            final byte data_AR[] = new byte[]{(byte) 0xE3, (byte) 0x00,
                    (byte) 0x00, (byte) 0x2C,
                    (byte) 0x00, (byte) 0x00,
                    (byte) 0x50, (byte) 0x79,
                    (byte) 0x00, (byte) 0x26,
                    (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00,
                    (byte) 0x80, (byte) 0x00,
                    (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
                    (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x08,  //bt add for phone, can be automate in the future
                    (byte) 0x3C, (byte) 0x5A, (byte) 0x37, (byte) 0xFF,
                    (byte) 0xFE, (byte) 0x95, (byte) 0xEE, (byte) 0xE3,
                    (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00};
//          Presentation APDU [0xE700]
            final byte data_DR[] = new byte[]{(byte) 0xE7, (byte) 0x00,
                    (byte) 0x00, (byte) 0x12,
                    (byte) 0x00, (byte) 0x10,
                    (byte) invoke[0], (byte) invoke[1],
                    (byte) 0x02, (byte) 0x01,
                    (byte) 0x00, (byte) 0x0A,
                    (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
                    (byte) 0x0D, (byte) 0x1D,
                    (byte) 0x00, (byte) 0x00};

            final byte get_MDS[] = new byte[]{(byte) 0xE7, (byte) 0x00,
                    (byte) 0x00, (byte) 0x0E,
                    (byte) 0x00, (byte) 0x0C,
                    (byte) 0x00, (byte) 0x24,
                    (byte) 0x01, (byte) 0x03,
                    (byte) 0x00, (byte) 0x06,
                    (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00};

            final byte data_RR[] = new byte[]{(byte) 0xE5, (byte) 0x00,
                    (byte) 0x00, (byte) 0x02,
                    (byte) 0x00, (byte) 0x00};

            try {
                Log.i(TAG, String.valueOf(count));
                if (count == 1) {
                    fos.write(data_AR);
                    Log.i(TAG, "Association Responsed!");
                } else if (count == 2) {
                    fos.write(get_MDS);
                    Log.i(TAG, "Get MDS object attributes!");
//                  fos.write(data_ABORT);
                } else if (count == 3) {
                    fos.write(data_DR);
                    Log.i(TAG, "Data Responsed!");
                } else if (count == 4) {
                    fos.write(data_RR);
                    Log.i(TAG, "Data Released!");
                }
            } catch (IOException ioe) {
            }
        }
    }

写线程的逻辑非常清晰,就是通过同一个ParcelFileDescriptor向血压计发送数据。发送的数据在这里已经写死,一共四组数据:作用依次是确认连接请求;确认收到测量数据;请求血压计的特征;确认断开连接。

大功告成,收到数据后界面如下:

Android蓝牙健康设备开发:Health Device Profile(HDP)_第5张图片
Screenshot_2016-09-18-09-53-32.png

第一次写文章,有各种不足之处,请大家指正。

完整代码

你可能感兴趣的:(Android蓝牙健康设备开发:Health Device Profile(HDP))