蓝牙hid协议源码解析
来源:互联网 发布:成都室内设计公司 知乎 编辑:程序博客网 时间:2024/05/16 08:46
1,概述
1.1 HID协议
HID协议: Hunman Interface Device Profile人机交互设备协议
使用场景:支持人机交互设备之间的控制
市场产品:蓝牙键盘,蓝牙鼠标,蓝牙游戏手柄等。
1.2 代码路径
客户端: frameworks\base\core\java\android\bluetooth
服务端: packages\apps\Bluetooth\src\com\android\bluetooth\ hid
HidDevService.java hid协议的服务端
开发流程和健康设备类似,但是稍微麻烦
2,接口
接口如下
3,开发步骤
在官方文档中有一个建立通信的流程:
1、调用getProfileProxy(Context,BluetoothProfile.ServiceListener, int)来获取代理对象的连接。
2、创建BluetoothHidDeviceAppSdpSettings, BluetoothHidDeviceAppQosSettings对象,创建BluetoothHidDeviceCallback回调,调用registerApp方法注册
3、将手机与设备配对,并且进行连接。
4、实现BluetoothHidDeviceCallback的7个回调方法
5、调用sendReport方法分别实现蓝牙鼠标,蓝牙键盘等。
3.1 获取客户端代理对象
一般在oncreate方法中,直接调用getProfileProxy方法,这个没什么好说的。
BluetoothAdapter.getDefaultAdapter().getProfileProxy(getApplicationContext(), mProfileServiceListener,BluetoothProfile. HID_DEVICE);
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) {mHidDevice = (BluetoothHidDevice) proxy; // 获取代理对象之后就进行注册 ...}}};
一般经过这个步骤,客户端的BluetoothHidDevice对象已经和服务端的HidDevService对象绑定了。
3.2 注册registerApp
BluetoothHidDeviceAppSdpSettings sdp = new BluetoothHidDeviceAppSdpSettings( HidConsts.NAME, HidConsts.DESCRIPTION, HidConsts.PROVIDER, BluetoothHidDevice.SUBCLASS1_COMBO, HidConsts.DESCRIPTOR);BluetoothHidDeviceAppQosSettings inQos = new BluetoothHidDeviceAppQosSettings( BluetoothHidDeviceAppQosSettings.SERVICE_GUARANTEED, 200, 2, 200, 10000 /* 10 ms */, 10000 /* 10 ms */);BluetoothHidDeviceAppQosSettings outQos = new BluetoothHidDeviceAppQosSettings( BluetoothHidDeviceAppQosSettings.SERVICE_GUARANTEED, 900, 9, 900, 10000 /* 10 ms */, 10000 /* 10 ms */);boolean result = mHidDevice.registerApp(sdp, inQos, outQos, mCallback);
HidConsts类的定义如下:
public class HidConsts { public final static String NAME = "HID Device Testapp"; public final static String DESCRIPTION = ""; public final static String PROVIDER = "Codeaurora"; /* @formatter:off */ public final static byte[] DESCRIPTOR = { (byte) 0x05, (byte) 0x01, // USAGE_PAGE (Generic Desktop) (byte) 0x09, (byte) 0x02, // USAGE (Mouse) (byte) 0xa1, (byte) 0x01, // COLLECTION (Application) (byte) 0x09, (byte) 0x01, // USAGE (Pointer) (byte) 0xa1, (byte) 0x00, // COLLECTION (Physical) (byte) 0x85, (byte) 0x02, // REPORT_ID (2) (byte) 0x05, (byte) 0x09, // USAGE_PAGE (Button) (byte) 0x19, (byte) 0x01, // USAGE_MINIMUM (Button 1) (byte) 0x29, (byte) 0x03, // USAGE_MAXIMUM (Button 3) (byte) 0x15, (byte) 0x00, // LOGICAL_MINIMUM (0) (byte) 0x25, (byte) 0x01, // LOGICAL_MAXIMUM (1) (byte) 0x95, (byte) 0x03, // REPORT_COUNT (3) (byte) 0x75, (byte) 0x01, // REPORT_SIZE (1) (byte) 0x81, (byte) 0x02, // INPUT (Data,Var,Abs) (byte) 0x95, (byte) 0x01, // REPORT_COUNT (1) (byte) 0x75, (byte) 0x05, // REPORT_SIZE (5) (byte) 0x81, (byte) 0x03, // INPUT (Cnst,Var,Abs) (byte) 0x05, (byte) 0x01, // USAGE_PAGE (Generic Desktop) (byte) 0x09, (byte) 0x30, // USAGE (X) (byte) 0x09, (byte) 0x31, // USAGE (Y) (byte) 0x15, (byte) 0x81, // LOGICAL_MINIMUM (-127) (byte) 0x25, (byte) 0x7f, // LOGICAL_MAXIMUM (127) (byte) 0x75, (byte) 0x08, // REPORT_SIZE (8) (byte) 0x95, (byte) 0x02, // REPORT_COUNT (2) (byte) 0x81, (byte) 0x06, // INPUT (Data,Var,Rel) (byte) 0x09, (byte) 0x38, // USAGE (Wheel) (byte) 0x15, (byte) 0x81, // LOGICAL_MINIMUM (-127) (byte) 0x25, (byte) 0x7f, // LOGICAL_MAXIMUM (127) (byte) 0x75, (byte) 0x08, // REPORT_SIZE (8) (byte) 0x95, (byte) 0x01, // REPORT_COUNT (1) (byte) 0x81, (byte) 0x06, // INPUT (Data,Var,Rel) (byte) 0xc0, // END_COLLECTION (byte) 0xc0, // END_COLLECTION // battery strength (byte) 0x05, (byte) 0x0c, (byte) 0x09, (byte) 0x01, (byte) 0xa1, (byte) 0x01, (byte) 0x85, (byte) 0x20, // REPORT_ID (32) (byte) 0x05, (byte) 0x01, (byte) 0x09, (byte) 0x06, (byte) 0xa1, (byte) 0x02, (byte) 0x05, (byte) 0x06, // USAGE_PAGE (Generic Device Controls) (byte) 0x09, (byte) 0x20, // USAGE (Battery Strength) (byte) 0x15, (byte) 0x00, // LOGICAL_MINIMUM (0) (byte) 0x26, (byte) 0xff, (byte) 0x00, // LOGICAL_MAXIMUM (100) (byte) 0x75, (byte) 0x08, // REPORT_SIZE (8) (byte) 0x95, (byte) 0x01, // REPORT_COUNT (1) (byte) 0x81, (byte) 0x02, // INPUT (Data,Var,Abs) (byte) 0xc0, (byte) 0xc0, (byte) 0x05, (byte) 0x01, // USAGE_PAGE (Generic Desktop) (byte) 0x09, (byte) 0x06, // USAGE (Keyboard) (byte) 0xa1, (byte) 0x01, // COLLECTION (Application) (byte) 0x85, (byte) 0x01, // REPORT_ID (1) (byte) 0x05, (byte) 0x07, // USAGE_PAGE (Keyboard) (byte) 0x19, (byte) 0xe0, // USAGE_MINIMUM (Keyboard LeftControl) (byte) 0x29, (byte) 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI) (byte) 0x15, (byte) 0x00, // LOGICAL_MINIMUM (0) (byte) 0x25, (byte) 0x01, // LOGICAL_MAXIMUM (1) (byte) 0x75, (byte) 0x01, // REPORT_SIZE (1) (byte) 0x95, (byte) 0x08, // REPORT_COUNT (8) (byte) 0x81, (byte) 0x02, // INPUT (Data,Var,Abs) (byte) 0x05, (byte) 0x0c, // USAGE_PAGE (Consumer Devices) (byte) 0x15, (byte) 0x00, // LOGICAL_MINIMUM (0) (byte) 0x25, (byte) 0x01, // LOGICAL_MAXIMUM (1) (byte) 0x95, (byte) 0x07, // REPORT_COUNT (7) (byte) 0x75, (byte) 0x01, // REPORT_SIZE (1) (byte) 0x09, (byte) 0xb6, // USAGE (Scan Previous Track) (byte) 0x09, (byte) 0xb5, // USAGE (Scan Next Track) (byte) 0x09, (byte) 0xb7, // USAGE (Stop) (byte) 0x09, (byte) 0xcd, // USAGE (Play/Pause) (byte) 0x09, (byte) 0xe2, // USAGE (Mute) (byte) 0x09, (byte) 0xe9, // USAGE (Volume Up) (byte) 0x09, (byte) 0xea, // USAGE (Volume Down) (byte) 0x81, (byte) 0x02, // INPUT (Data,Var,Abs) (byte) 0x95, (byte) 0x01, // REPORT_COUNT (1) (byte) 0x75, (byte) 0x01, // REPORT_SIZE (1) (byte) 0x81, (byte) 0x03, // INPUT (Constant,Var,Abs) (byte) 0x05, (byte) 0x07, // USAGE_PAGE (Keyboard) (byte) 0x95, (byte) 0x05, // REPORT_COUNT (5) (byte) 0x75, (byte) 0x01, // REPORT_SIZE (1) (byte) 0x85, (byte) 0x01, // REPORT_ID (1) (byte) 0x05, (byte) 0x08, // USAGE_PAGE (LEDs) (byte) 0x19, (byte) 0x01, // USAGE_MINIMUM (Num Lock) (byte) 0x29, (byte) 0x05, // USAGE_MAXIMUM (Kana) (byte) 0x91, (byte) 0x02, // OUTPUT (Data,Var,Abs) (byte) 0x95, (byte) 0x01, // REPORT_COUNT (1) (byte) 0x75, (byte) 0x03, // REPORT_SIZE (3) (byte) 0x91, (byte) 0x03, // OUTPUT (Cnst,Var,Abs) (byte) 0x95, (byte) 0x06, // REPORT_COUNT (6) (byte) 0x75, (byte) 0x08, // REPORT_SIZE (8)
连接很简单,直接调用connect方法就可以了。
public void connect() { if (mHidDevice == null) return; mHidDevice.connect();}
3.4 BluetoothHidDeviceCallback
BluetoothHidDeviceCallback这个抽象类有7个回调方法,
private byte[] mBuffer = new byte[1];
private final BluetoothHidDeviceCallback mCallback = new BluetoothHidDeviceCallback() { @Override public void onAppStatusChanged(BluetoothDevice pluggedDevice, BluetoothHidDeviceAppConfiguration config, boolean registered) { // 一般在registerApp和unregisterApp方法之后回调 // registered 表示是否注册上 } @Override public void onConnectionStateChanged(BluetoothDevice device, int state) { // device 远程蓝牙设备 state连接状态 mBuffer = (byte) 63 mHidDevice.sendReport(32, mBuffer); // 不知道为啥子这样写? } // 其他5个方法就可以不管了。 @Override public void onIntrData(byte reportId, byte[] data) { Log.v(TAG, "intr data: reportId=" + reportId + " data=" + Arrays.toString(data)); } @Override public void onSetProtocol(byte protocol) { Log.d(TAG, "protocol set to " + protocol); } @Override public void onVirtualCableUnplug() { } @Override public void onGetReport(byte type, byte id, int bufferSize) { } @Override public void onSetReport(byte type, byte id, byte[] data) { } };
BluetoothHidDeviceCallback7个方法都是C/C++等通过JNI机制的回调。
4,蓝牙鼠标
4.1 蓝牙鼠标滑动
实现鼠标在整个界面前后左右上下滑动
mTouchpad = view.findViewById(R.id.touchpad); mTouchpad.setOnTouchListener(new OnTouchListener() { private int mPrevX; private int mPrevY; @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mPrevX = (int) (event.getX() * mSpeed); mPrevY = (int) (event.getY() * mSpeed); break; case MotionEvent.ACTION_MOVE: int x = (int) (event.getX() * mSpeed); int y = (int) (event.getY() * mSpeed); mouseMove((byte) (x – mPrevX), (byte) (y - mPrevY)); mPrevX = x; mPrevY = y; break; } return true; } });
private int mSpeed = 3;
mSpeed的值控制鼠标移动的速度。
private byte[] mBuffer = new byte[4];byte id = 2;
public synchronized void move(byte dx, byte dy) { // leave buttons state unchanged mBuffer[1] = dx; mBuffer[2] = dy; mHidDevice.sendReport(id, mBuffer); }
4.2 蓝牙鼠标点击
将鼠标滑动到目标后,点击按钮可以选中目标。
button.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { int which = 0; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mouseButtonDown(which); break; case MotionEvent.ACTION_UP: mouseButtonUp(which); break; } return false; } });
Which的值有三种,分别是0,1,2 之间好像没什么差别。
public synchronized void buttonDown(int which) { mBuffer[0] |= (1 << which); mBuffer[1] = 0; mBuffer[2] = 0; mHidDevice.sendReport(id, mBuffer); } public synchronized void buttonUp(int which) { mBuffer[0] &= ~(1 << which); mBuffer[1] = 0; mBuffer[2] = 0; mHidDevice.sendReport(id, mBuffer); }
4.3 蓝牙鼠标翻页
比如在手机中,有时候界面不止一页,这样就需要翻页来显示了。
private int mScrollSpeed = 3; // 控制翻页的速度mScrollZone.setOnTouchListener(new OnTouchListener() { private int mPrevY; @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mPrevY = (int) (event.getY() * mScrollSpeed); break; case MotionEvent.ACTION_MOVE: int y = (int) (event.getY() * mScrollSpeed); mouseScroll((byte) (mPrevY - y)); mPrevY = y; break; } return true; } });public synchronized void scroll(byte delta) { mBuffer[3] = delta; mHidDevice.sendReport(id, mBuffer); mBuffer[3] = 0x00; }
5,蓝牙键盘
市面上的键盘类型较多,实现了3种蓝牙键盘,
标准键盘
lable以及对应keyCode如下:
<Keyboard> <Layout> <Key keyLabel="Esc" keyCode="0x29"/> <Key keyLabel="F1" keyCode="0x3A"/> <Key keyLabel="F2" keyCode="0x3B"/> <Key keyLabel="F3" keyCode="0x3C"/> <Key keyLabel="F4" keyCode="0x3D"/> <Key keyLabel="F5" keyCode="0x3E"/> <Key keyLabel="F6" keyCode="0x3F"/> <Key keyLabel="F7" keyCode="0x40"/> <Key keyLabel="F8" keyCode="0x41"/> <Key keyLabel="F9" keyCode="0x42"/> <Key keyLabel="F10" keyCode="0x43"/> <Key keyLabel="F11" keyCode="0x44"/> <Key keyLabel="F12" keyCode="0x45"/> <Key keyLabel="Del" keyCode="0x4C"/> </Layout> <Layout> <Key keyLabel="'" shiftLabel="~" keyCode="0x35"/> <Key keyLabel="1" shiftLabel="!" keyCode="0x1E"/> <Key keyLabel="2" shiftLabel="\@" keyCode="0x1F"/> <Key keyLabel="3" shiftLabel="\#" keyCode="0x20"/> <Key keyLabel="4" shiftLabel="$" keyCode="0x21"/> <Key keyLabel="5" shiftLabel="%" keyCode="0x22"/> <Key keyLabel="6" shiftLabel="^" keyCode="0x23"/> <Key keyLabel="7" shiftLabel="&" keyCode="0x24"/> <Key keyLabel="8" shiftLabel="*" keyCode="0x25"/> <Key keyLabel="9" shiftLabel="(" keyCode="0x26"/> <Key keyLabel="0" shiftLabel=")" keyCode="0x27"/> <Key keyLabel="-" shiftLabel="_" keyCode="0x2D"/> <Key keyLabel="=" shiftLabel="+" keyCode="0x2E"/> <Key keyLabel="Backspace ←" keyCode="0x2A" weight="1.5"/> </Layout> <Layout> <Key keyLabel="Tab ↹" keyCode="0x2B" weight="1.5"/> <Key keyLabel="Q" keyCode="0x14"/> <Key keyLabel="W" keyCode="0x1A"/> <Key keyLabel="E" keyCode="0x08"/> <Key keyLabel="R" keyCode="0x15"/> <Key keyLabel="T" keyCode="0x17"/> <Key keyLabel="Y" keyCode="0x1C"/> <Key keyLabel="U" keyCode="0x18"/> <Key keyLabel="I" keyCode="0x0C"/> <Key keyLabel="O" keyCode="0x12"/> <Key keyLabel="P" keyCode="0x13"/> <Key keyLabel="[" keyCode="0x2F" shiftLabel="{"/> <Key keyLabel="]" keyCode="0x30" shiftLabel="}"/> <Key keyLabel="\\" keyCode="0x31" shiftLabel="|"/> </Layout> <Layout> <Key keyLabel="Caps Lock" keyCode="0x39" weight="1.5"/> <Key keyLabel="A" keyCode="0x04"/> <Key keyLabel="S" keyCode="0x16"/> <Key keyLabel="D" keyCode="0x07"/> <Key keyLabel="F" keyCode="0x09"/> <Key keyLabel="G" keyCode="0x0A"/> <Key keyLabel="H" keyCode="0x0B"/> <Key keyLabel="J" keyCode="0x0D"/> <Key keyLabel="K" keyCode="0x0E"/> <Key keyLabel="L" keyCode="0x0F"/> <Key keyLabel=";" keyCode="0x33" shiftLabel=":"/> <Key keyLabel="'" keyCode="0x34" shiftLabel="""/> <Key keyLabel="Enter ↵" keyCode="0x28" weight="3.0"/> </Layout> <Layout> <Key keyLabel="Shift ⇧" keyCode="0xE1" keyFunc="Shift" weight="1.5"/> <Key keyLabel="Z" keyCode="0x1D"/> <Key keyLabel="X" keyCode="0x1B"/> <Key keyLabel="C" keyCode="0x06"/> <Key keyLabel="V" keyCode="0x19"/> <Key keyLabel="B" keyCode="0x05"/> <Key keyLabel="N" keyCode="0x11"/> <Key keyLabel="M" keyCode="0x10"/> <Key keyLabel="," keyCode="0x36" shiftLabel="<"/> <Key keyLabel="." keyCode="0x37" shiftLabel=">"/> <Key keyLabel="/" keyCode="0x38" shiftLabel="\?"/> <Key keyLabel="Shift ⇧" keyCode="0xE5" keyFunc="Shift" weight="1.5"/> </Layout> <Layout> <Key keyLabel="Ctrl" keyCode="0xE0" weight="1.5"/> <Key keyLabel="Win" keyCode="0xE3"/> <Key keyLabel="Alt" keyCode="0xE2"/> <Key keyLabel=" " keyCode="0x2C" weight="10.0"/> <Key keyLabel="Alt Gr" keyCode="0xE6"/> <Key keyLabel="Win" keyCode="0xE7"/> <Key keyLabel="Menu" keyCode="0x76"/> <Key keyLabel="Ctrl" keyCode="0xE4" weight="1.5"/> </Layout></Keyboard>
左边是导航键盘 右边是数字键盘
导航键盘的lable以及对应keyCode如下:
<Keyboard> <Layout> <Key keyLabel="Vol Down ⇩" keyCode="0x86"/> <Key keyLabel="Mute" keyCode="0x84"/> <Key keyLabel="Vol Up ⇧" keyCode="0x85"/> </Layout> <Layout> <Key keyLabel="Print Screen" keyCode="0x46"/> <Key keyLabel="Scroll Lock" keyCode="0x47"/> <Key keyLabel="Pause" keyCode="0x48"/> </Layout> <Layout> <Key keyLabel="Insert" keyCode="0x49"/> <Key keyLabel="Home" keyCode="0x4A"/> <Key keyLabel="Page Up" keyCode="0x4B"/> </Layout> <Layout> <Key keyLabel="Delete" keyCode="0x4C"/> <Key keyLabel="End" keyCode="0x4D"/> <Key keyLabel="Page Down" keyCode="0x4E"/> </Layout> <Layout> <Key visible="false"/> <Key keyLabel="↑" keyCode="0x52"/> <Key visible="false"/> </Layout> <Layout> <Key keyLabel="←" keyCode="0x50"/> <Key keyLabel="↓" keyCode="0x51"/> <Key keyLabel="→" keyCode="0x4F"/> </Layout></Keyboard>
数字键盘的lable以及对应keyCode如下:
<Keyboard> <Layout> <Key keyLabel="Prev\n⇦" keyCode="0x80"/> <Key keyLabel="Next\n⇨" keyCode="0x81"/> <Key keyLabel="Stop" keyCode="0x82"/> <Key keyLabel="Play\nPause" keyCode="0x83"/> <Key visible="false"/> </Layout> <Layout> <Key keyLabel="Num\nLock" keyCode="0x53"/> <Key keyLabel="/" keyCode="0x54"/> <Key keyLabel="*" keyCode="0x55"/> <Key keyLabel="-" keyCode="0x56"/> </Layout> <Layout> <Key keyLabel="7\nHome" keyCode="0x5F"/> <Key keyLabel="8 ↑" keyCode="0x60"/> <Key keyLabel="9\nPgUp" keyCode="0x61"/> <Key keyLabel="+" keyCode="0x57"/> </Layout> <Layout> <Key keyLabel="4\n←" keyCode="0x5C"/> <Key keyLabel="5" keyCode="0x5D"/> <Key keyLabel="6\n→" keyCode="0x5E"/> <Key visible="false"/> </Layout> <Layout> <Key keyLabel="1\nEnd" keyCode="0x59"/> <Key keyLabel="2 ↓" keyCode="0x5A"/> <Key keyLabel="3\nPgDn" keyCode="0x5B"/> <Key visible="false"/> </Layout> <Layout> <Key keyLabel="0\nIns" keyCode="0x62" weight="2.0"/> <Key keyLabel=".\nDel" keyCode="0x63"/> <Key keyLabel="Enter" keyCode="0x58"/> </Layout></Keyboard>
按键事件都是一样的,主要就是keyCode不同,
@Override public void onKeyUp(byte keyCode) { keyboardKeyUp(keyCode); } @Override public void onKeyDown(byte keyCode) { keyboardKeyDown(keyCode); }private final static byte MODIFIER_BASE = (byte) 0xe0; private final static byte MODIFIER_COUNT = 8; /* left ctrl -> right gui */ private byte[] mBuffer = new byte[8]; byte id = 1;public synchronized void keyDown(byte key) { if (key >= MODIFIER_BASE && key <= MODIFIER_BASE + MODIFIER_COUNT) { mBuffer[0] |= (1 << (key - MODIFIER_BASE)); } else if ((key & 0x80) != 0) { mBuffer[1] |= (1 << (key & 0x07)); } else { for (int i = 2; i < 8; i++) { if (mBuffer[i] == 0x00) { mBuffer[i] = key; break; } } } mHidDevice.sendReport(id, mBuffer); } public synchronized void keyUp(byte key) { if (key >= MODIFIER_BASE && key <= MODIFIER_BASE + MODIFIER_COUNT) { mBuffer[0] &= ~(1 << (key - MODIFIER_BASE)); } else if ((key & 0x80) != 0) { mBuffer[1] &= ~(1 << (key & 0x07)); } else { for (int i = 2; i < 8; i++) { if (mBuffer[i] == key) { mBuffer[i] = 0x00; break; } } } mHidDevice.sendReport(id, mBuffer); }
这样就可以了,很简单吧。
6,源码解析
客户端的BluetoothHidDevice和服务端的HidDevService都比较简单,很直接,完全没有拐弯抹角的地方, sendReport方法从客户端到服务端的过程如下,
回调的7个方法流程如下,以onAppStatusChanged回调为例,
7,小节
问题:1,回调的几个方法以及2个类还未弄清楚其作用。
2,客户端的BluetoothInputDevice以及客户端的HidService和HID协议的文件在一个文件夹下,那么这2个类的作用是什么呢?如何使用。
- 蓝牙hid协议源码解析
- 蓝牙HID协议笔记
- 蓝牙HID协议笔记
- 蓝牙HID协议笔记
- 蓝牙HID协议介绍
- 蓝牙HID协议笔记
- 蓝牙PAN协议源码解析
- 蓝牙HDP协议源码解析
- 蓝牙sdp协议源码解析
- 蓝牙pbap协议源码解析
- 蓝牙map协议源码解析
- 蓝牙基本功能源码解析
- 蓝牙a2dp协议源码分析
- 蓝牙avrcp协议源码分析
- 蓝牙avrcp协议源码分析
- android源码蓝牙协议分析
- 蓝牙 BCSP 解析 源码分析
- 蓝牙通话功能源码解析
- html中转义字符的处理 前端和后端两种处理方式
- JAVA基础【3.3】《Java核心技术1》Java的基本程序设计结构-数据类型
- 关于使用druid打印出sql的优势
- 完成端口函数解析
- 我的Android笔记(七)—— 读取txt文件内容
- 蓝牙hid协议源码解析
- .vimrc 语法
- 取数字问题
- JAVA基础【3.4】《Java核心技术1》Java的基本程序设计结构-变量和常量
- Android第十三课;绝对布局
- svn 主干和分支合并
- 基于AOP配置的Spring缓存
- Web API 2中的Action Result
- VMware虚拟机安装CentOS后无法联网问题