蓝牙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协议了。
- 蓝牙HDP协议源码解析
- 蓝牙PAN协议源码解析
- 蓝牙hid协议源码解析
- 蓝牙sdp协议源码解析
- 蓝牙pbap协议源码解析
- 蓝牙map协议源码解析
- android蓝牙协议名词解释 OPP HFP HDP A2DP PAN
- 蓝牙基本功能源码解析
- 蓝牙a2dp协议源码分析
- 蓝牙avrcp协议源码分析
- 蓝牙avrcp协议源码分析
- android源码蓝牙协议分析
- 蓝牙 BCSP 解析 源码分析
- 蓝牙通话功能源码解析
- 蓝牙解析(part3):BLE协议栈解析
- Android 蓝牙源码结构与协议简介
- 蓝牙连接以及协议数据解析
- 蓝牙解析(part2):协议架构分析
- 紫书 例题 10-11 UVa11181
- angular的轮播
- SLAM系列之1 - ORB SLAM
- 在仅cpu模式下运行fast rcnn代码
- 学习笔记∣stm32l0xx时钟系统详解与代码配置
- 蓝牙HDP协议源码解析
- READING NOTE: YOLO9000: Better, Faster, Stronger
- ver0.83--preproc_data.m
- 瓦片地图
- 大数据学习笔记之十一 云计算应用分类
- NOSQL数据库浅析(一):Memcache 内存分配策略和性能(使用)状态检查
- linux系统maven安装笔记
- CSS 实现三角形对话框
- afinal----finalDB向数据库里save数据出错:sava error:sqlInfo is null