蓝牙HDP协议源码解析

来源:互联网 发布:大学生在知乎怎么赚钱 编辑:程序博客网 时间:2024/05/18 02:55

1,概述

1.1 HDP协议

HDP协议: Health Device Profile健康设备协议

使用场景:支持各种蓝牙健康设备和手机进行通信

市场产品:广泛应用于各种智能穿戴设备,比如蓝牙健康手环,蓝牙血压计,蓝牙温度计,蓝牙电子称等各种健康设备。

 

1.2 代码路径

客户端: frameworks\base\core\java\android\bluetooth

  BluetoothHealth.java            hdp协议客户端

  BluetoothHealthAppConfiguration.java  设备连接通信的相关信息

  BluetoothHealthCallback.java     用于第三方app注册的回调方法

服务端: packages\apps\Bluetooth\src\com\android\bluetooth\ hdp

HealthService.java                 hdp协议的服务端

这4个类不是很难,主要看逻辑以及接口

2,接口

接口如下


首先要清楚一个概念,手机和健康设备(比如蓝牙血压计)连接,通常是将健康设备的数据发送给手机,一般来说,健康设备是数据的输出端,手机是数据的输入端,而开发也是针对手机端的开发。

看这些接口,居然没有Connect接口,这不科学啊!

connectChannelToSource手机主动连接健康设备时的接口。相当于其他协议的Connect接口。

connectChannelToSink蓝牙设备主动连接手机的接口。

registerSinkAppConfiguration注册方法,主要监听蓝牙信道的状态。最终是调用registerAppConfiguration接口来完成。

其他的接口从名字就知道什么意思,就不多论述了。

3,开发步骤

在官方文档中有一个建立通信的流程:

1、调用getProfileProxy(Context,BluetoothProfile.ServiceListener, int)来获取代理对象的连接。

2、创建BluetoothHealthCallback回调,调用registerSinkAppConfiguration方法注册一个sink端的应用程序配置。

3、将手机与健康设备配对,这一步在手机的设置中就可以完成。

4、使用connectChannelToSource方法来建立一个与健康设备的通信channel。有的设备会自动建立通信,不需要在代码中调用这个方法。第二步中的回调会指示channel的状态变化。

5、用ParcelFileDescriptor来读取健康设备传来的数据,并根据IEEE 11073-****协议来解析数据。

6、通信结束后,关闭通信channel,取消第二步中的注册。

蓝牙的基本操作(打开,配对,连接等)就不论述了,主要说明手机端和蓝牙血压计如何利用IEEE 11073协议进行通信。

3.1 获取客户端代理对象

一般在oncreate方法中,直接调用getProfileProxy方法,这个没什么好说的。

BluetoothAdapter.getDefaultAdapter().getProfileProxy(getApplicationContext(), mProfileServiceListener,BluetoothProfile.HEALTH);


private BluetoothProfile.ServiceListener mProfileServiceListener = new BluetoothProfile.ServiceListener() {@Overridepublic void onServiceDisconnected(int profile) {if (profile == BluetoothProfile.HEALTH){     mBluetoothHealth = null;}}@SuppressLint("NewApi")@Overridepublic void onServiceConnected(int profile, BluetoothProfile proxy) {if (profile == BluetoothProfile.HEALTH) {mBluetoothHealth = (BluetoothHealth) proxy;}}};

一般经过这个步骤,客户端的BluetoothHealth对象已经和服务端的HealthService对象绑定了。

3.2 注册BluetoothHealthAppConfiguration

private static final String TAG = "BluetoothHealth"; // 这只是个标志,可以任意private static final int dataType = 0x1007; //IEEE 11073中规定的血压数据类型private BluetoothHealthAppConfiguration mHealthAppConfig;private int mChannelId;mBluetoothHealth.registerSinkAppConfiguration(TAG, dataType, mHealthCallback);private final BluetoothHealthCallback mHealthCallback = new BluetoothHealthCallback() {    public void onHealthAppConfigurationStatusChange(BluetoothHealthAppConfiguration                                            config, int status) {            if (status == BluetoothHealth.APP_CONFIG_REGISTRATION_FAILURE) {                mHealthAppConfig = null; // 注册失败             } else if (status == BluetoothHealth.APP_CONFIG_REGISTRATION_SUCCESS) {                mHealthAppConfig = config; // 注册成功            } else if (status == BluetoothHealth.APP_CONFIG_UNREGISTRATION_FAILURE ||                status == BluetoothHealth.APP_CONFIG_UNREGISTRATION_SUCCESS) {            // 取消注册的广播,开发很少用到            }        }          public void onHealthChannelStateChange(BluetoothHealthAppConfiguration config,           BluetoothDevice device, int prevState, int newState, ParcelFileDescriptor fd,                  int channelId) {        if (prevState == BluetoothHealth.STATE_CHANNEL_DISCONNECTED &&                       newState == BluetoothHealth.STATE_CHANNEL_CONNECTED) {                   if (config.equals(mHealthAppConfig)) {                        mChannelId = channelId; // 连接成功                       (new ReadThread(fd)).start();// 开始读取数据                } else {                      // 连接失败  一般下面的几个连接状态开发中也很少用到,可以输出log                }            } else if (prevState == BluetoothHealth.STATE_CHANNEL_CONNECTING &&                       newState == BluetoothHealth.STATE_CHANNEL_DISCONNECTED) {                •••            } else if (newState == BluetoothHealth.STATE_CHANNEL_DISCONNECTED) {            •••                if (config.equals(mHealthAppConfig)) {                      •••                 } else {                      •••                      }            }        }    };

3.3 建立channel

在设置中配对连接这个就不论述了,主要论述一下Pan协议的连接。

在手机端,调用connectChannelToSource接口就可以了,

mBluetoothHealth.connectChannelToSource(device,mHealthAppConfig);

在蓝牙血压计端,调用的是 connectChannelToSink接口

3.4 读取解析数据

在第二步中注册后, 在onHealthChannelStateChange方法中监听了channel的变化,一旦channel建立成功,则利用回调的ParcelFileDescriptor对象另开一个读取数据的线程,读取channel中的数据。

下面是手机端获取血压计的参数:收缩压,舒张压,心率以及测量时间。

int count = 0; // 交互通信的次数byte invoke[] = new byte[] { (byte) 0x00, (byte) 0x00 };        private class ReadThread extends Thread {        private ParcelFileDescriptor mFd;        public ReadThread(ParcelFileDescriptor fd) {            super();            mFd = fd;        }                @Override        public void run() {            FileInputStream fis = new FileInputStream(mFd.getFileDescriptor());            byte data[] = new byte[300];            try {                while(fis.read(data) > -1) {                if (data[0] != (byte) 0x00) {                String test = byte2hex(data);                if(data[0] == (byte) 0xE2){ // 请求连接的信号                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){                if (data[18] == (byte) 0x0d && data[19] == (byte) 0x1d) {                count = 3;                 invoke = new byte[] { data[6], data[7] };                (new WriteThread(mFd)).start();                int length = data[21];                int number_of_data_packets = data[22+5];                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]);                break;                case PULSE_DATA:                int pulse = byteToUnsignedInt(data[packet_start+5]); // 心率                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;                break;                case ERROR_CODE_DATA:                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;                    }                }                 }            } catch(IOException ioe) {}            if (mFd != null) {                try {                    mFd.close();                } catch (IOException e) { /* Do nothing. */ }            }        }    }

sys, dia等数据就是最后的结果,我们就可以拿这些数据进行处理了。

通信是双方的,设备向手机发送信息,手机也可以回复设备的信息,所以还要另外一个写数据的线程,实现手机端和血压计之间的数据交换。

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,    (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 };   final byte data_RRQ[] = new byte[] {(byte) 0xE4, (byte) 0x00,         (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x00 };              final byte data_ABORT[] = new byte[] { (byte) 0xE6, (byte) 0x00, (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x00 };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());                        try {            Log.i(TAG, String.valueOf(count));            if (count == 1) {            fos.write(data_AR);            }  else if (count == 2) {            fos.write(get_MDS);                //fos.write(data_ABORT);            }else if (count == 3) {            fos.write(data_DR);            }else if (count == 4) {            fos.write(data_RR);            }            } catch(IOException ioe) {}        }    }        public String byte2hex(byte[] b){       String hs = "";       String stmp = "";       for (int n = 0; n < b.length; n++){          stmp = (java.lang.Integer.toHexString(b[n] & 0XFF));          if (stmp.length() == 1) {             hs = hs + "0" + stmp;          }else {             hs = hs + stmp;          }          if (n < b.length - 1) {             hs = hs + "";          }       }       return hs;    }        public static int byteToUnsignedInt(byte b) {  // byte转int        return 0x00 << 24 | b & 0xff;      }        public static String byteToString(byte b) { // byte转String        return String.valueOf(0x00 << 24 | b & 0xff);      }

看这些读和写的代码有些头晕,幸好count记下了相互交流的次数,其实逻辑很简单.

1,建立channel时,血压计发送请求连接的信号,手机端回复同意连接

2,手机端发送血压计相关数据,血压计发送相关数据。

3,手机端确认接收数据。

4,发送完数据之后,血压计请求断开连接,最后手机端确认断开连接。

至于发送的数据是什么意思,参考IEEE 11073中的内容,其中IEEE 11073-10407是专门针对血压计的协议。

 

到此为止,我们已经可以拿到最后的结果进行处理了,但是这些远远不够,说好的源码呢,这些背后的逻辑是什么呢?

4,源码解析

4.1 注册流程分析


主要分为2个步骤:

1,跨进程调用,从客户端调用到服务端对应的方法去注册

2,服务端将注册的结果回调给客户端, 然后我们根据注册做其他事情

其实,在这儿我觉得将注册的结果直接发送广播更好,方便直接.

首先看看registerSinkAppConfiguration方法,


public boolean registerSinkAppConfiguration(String name, int dataType,            BluetoothHealthCallback callback) {        if (!isEnabled() || name == null) return false;        if (VDBG) log("registerSinkApplication(" + name + ":" + dataType + ")");        return registerAppConfiguration(name, dataType, SINK_ROLE,                CHANNEL_TYPE_ANY, callback);    }

这里有5个参数:

第一个参数:任意一个字符,只是标志作用

第二个参数: 数据类型,不同健康设备的类型不一样,详细查阅IEEE 11073协议

第三个参数:设备角色,有如下2个值,

  public static final int SOURCE_ROLE = 1 << 0; // 健康设备端注册  public static final int SINK_ROLE = 1 << 1;   // 手机端注册

第四个参数: channel通道的类型,有3种类型可选

    public static final int CHANNEL_TYPE_RELIABLE = 10;    public static final int CHANNEL_TYPE_STREAMING = 11;    public static final int CHANNEL_TYPE_ANY = 12; // 默认类型

第五个参数:开发者必须实现的BluetoothHealthCallback类以及2个方法。

 

然后看看MESSAGE_REGISTER_APPLICATION这个消息的处理,

BluetoothHealthAppConfiguration appConfig =                        (BluetoothHealthAppConfiguration) msg.obj; AppInfo appInfo = mApps.get(appConfig);  if (appInfo == null) break;       int halRole = convertRoleToHal(appConfig.getRole());       int halChannelType = convertChannelTypeToHal(appConfig.getChannelType());       int appId = registerHealthAppNative(appConfig.getDataType(), halRole,                        appConfig.getName(), halChannelType);      if (appId == -1) { // 注册失败       callStatusCallback(appConfig,BluetoothHealth.APP_CONFIG_REGISTRATION_FAILURE);                        appInfo.cleanup();                        mApps.remove(appConfig);       } else { // 注册成功               //link to death with a recipient object to implement binderDead()      appInfo.mRcpObj = new BluetoothHealthDeathRecipient(HealthService.this,appConfig);       IBinder binder = appInfo.mCallback.asBinder();         try {              binder.linkToDeath(appInfo.mRcpObj,0);              } catch (RemoteException e) {                   Log.e(TAG,"LinktoDeath Exception:"+e);                }          appInfo.mAppId = appId;        callStatusCallback(appConfig, BluetoothHealth.APP_CONFIG_REGISTRATION_SUCCESS);    }

linkToDeath这个方法是干嘛的,有什么作用呢?

Binder自然是指BluetoothHealth 的内部类BluetoothHealthCallbackWrapper对象,

appInfo.mRcpObj指BluetoothHealthDeathRecipient对象,

private static class BluetoothHealthDeathRecipient implements IBinder.DeathRecipient{        private BluetoothHealthAppConfiguration mConfig;        private HealthService mService;        public BluetoothHealthDeathRecipient(HealthService service,                         BluetoothHealthAppConfiguration config) {            mService = service;            mConfig = config;        }        public void binderDied() {            if (DBG) Log.d(TAG,"Binder is dead.");            mService.unregisterAppConfiguration(mConfig);        }        public void cleanup(){            mService = null;            mConfig = null;        }    }

直接说了,一旦客户端的app突然崩溃了, BluetoothHealt对象还未来得及调用

unregisterAppConfiguration方法,服务端的注册还在,怎么办呢?通过linkToDeath方法,一旦客户端的app挂了,就会直接调用BluetoothHealthDeathRecipient的binderDied方法,这样,就可以取消服务端的注册。嗯,的确是一个很好的方法。进程调用以下语句可以杀死自己。

android.os.Process.killProcess(android.os.Process.myPid());

 注册函数说到底最后还是调用registerHealthAppNative方法完成,根据返回的结果调用callStatusCallback方法,最后调用开发者写的

onHealthAppConfigurationStatusChange方法,我还是觉得不如广播方便,广播多简单啊。

3.2建立channel

建立channel有2个方法,

1,手机端调用connectChannelToSource方法时会新建channel

2,健康设备调用connectChannelToSink方法时也会新建channel

connectChannelToSource和connectChannelToSink接口的调用流程完全和上小节中注册的流程一样,都会调用HealthService的connectChannel方法,最后会connectChannelNative方法完成最后的连接,根据连接返回的状态通过onHealthChannelStateChange方法监听。

下面主要论述底层连接的反馈过程,流程图如下:



直接看onChannelStateChanged方法,

private void onChannelStateChanged(int appId, byte[] addr, int cfgIndex,                                   int channelId, int state, FileDescriptor pfd) {        Message msg = mHandler.obtainMessage(MESSAGE_CHANNEL_STATE_CALLBACK);        ChannelStateEvent channelStateEvent = new ChannelStateEvent(appId, addr, cfgIndex,                                                              channelId, state, pfd);        msg.obj = channelStateEvent;        mHandler.sendMessage(msg);    }

重点在后面三个参数,分别是新建channel的Id,设备连接的状态以及用于通信的ParcelFileDescriptor对象,这三个主要的对象都从底层传上来了,其他的都是浮云的,按照流程走就可以了。

从onChannelStateChanged方法来看,还是不能发广播的,只能跨进程调用。

5,小节

这些都捋清楚了,如果是换一个健康设备,比如蓝牙体重秤呢,如何进行通信?

关键得看IEEE 11073协议了。

0 0