湖畔镇

蓝牙学习笔记

蓝牙是一种无线技术标准,可实现固定设备、移动设备和楼宇个域网之间的短距离数据交换,传输距离近、低功耗

蓝牙学习笔记

基础知识

蓝牙是一种无线技术标准,可实现固定设备、移动设备和楼宇个域网之间的短距离数据交换(使用2.4—2.485GHz的ISM波段的UHF无线电波)。蓝牙技术最初由电信巨头爱立信公司1994年创制,当时是作为RS232数据线的替代方案。蓝牙可连接多个设备,克服了数据同步的难题。蓝牙是一个标准的无线通讯协议,基于设备低成本的收发器芯片,传输距离近、低功耗

蓝牙使用跳频技术,将传输的数据分割成数据包,通过79个指定的蓝牙频道分别传输数据包。每个频道的频宽为1 MHz。蓝牙4.0使用2 MHz 间距,可容纳40个频道

最初,高斯频移键控GFSK调制是唯一可用的调制方案。然而蓝牙2.0+EDR 使得 π/4-DQPSK和8DPSK 调制在兼容设备中的使用变为可能

蓝牙是基于数据包、有着主从架构的协议。一个主设备至多可和同一微微网中的七个从设备通讯。所有设备共享主设备的时钟

蓝牙Profile

Bluetooth的一个很重要特性,就是所有的Bluetooth产品都无须实现全部的Bluetooth规范。为了更容易的保持Bluetooth设备之间的兼容,Bluetooth规范中定义了Profile。Profile定义了设备如何实现一种连接或者应用,你可以把Profile理解为连接层或者应用层协议

在所有的Profile中,有四种是基本的Profile,这些Profile会被其它的Profile使用:

  • GAP Generic Access Profile,该Profile保证不同的设备可以互相发现对方并建立连接,它具有强制性,并作为所有其它蓝牙应用规范的基础
  • SDAP Service Discovery Application Profile,通过该Profile,一个设备可以找到其它设备提供的服务,以及查询相关的信息
  • SPP Serial Port Profile,定义了如何在两台设备之间建立虚拟串口并进行连接
  • GOEP Generic Object Exchange Profile,通用对象交换。这个Profile的名字有些费解,它定义的是数据的传输,包括同步,文件传输,或者推送其它的数据。可以理解为与内容无关的传输层协议,可以被任何应用用来传输自己定义的数据对象
  • A2DP Advenced Audio Distribution Profile,蓝牙音频传输模型协定。A2DP 规定了使用蓝牙非同步传输信道方式,传输高质量音乐文件数据的协议堆栈软件和使用方法,基于该协议就能通过以蓝牙方式传输高品质的音乐了。这个技术可以利用立体声蓝牙耳机来收听手机中的音乐了
  • DUN Dial-up Networking Profile,实现一台蓝牙设备通过另外一个带无线功能的蓝牙设备共享上网
  • GATT Generic Attribute Profile,是一个在蓝牙连接之上的发送和接收很短的数据段的通用规范,用于低功耗蓝牙BLE

BLE

BLE(Bluetooth Low Energy),蓝牙4.0核心特性,Android4.3以上才支持,主要特点是快速搜索,快速连接,超低功耗保持连接和数据传输,缺点:数据传输速率低,由于其具有低功耗特点,所以经常用在可穿戴设备之中

  1. Profile:可以理解为一种规范,一个标准的通信协议,其存在于手机中,蓝牙组织规定了一些标准的Profile,每个Profile中包含了多个Service
  2. Service:可以理解为一个服务,在BLE从机中有多个服务,电量信息,系统服务信息等,每一个Service中包含了多个Characteristic特征值,每一个具体的Characteristic特征值才是BLE通信的主题
  3. Characteristic特征值:BLE主机从机通信均是通过Characteristic进行,可以将其理解为一个标签,通过该标签可以读取或写入相关信息
  4. UUID:Service和Characteristic均需要这个唯一的UUID进行标识,识别符有通用的,也可以自定义,也可以随机生成,每一个特征都有其属性和权限READ/WRITE/NOTIFY/INDICATE,特征根据属性可读可写

任何设备都可以单独作为中心或外设角色。一个没有被链接的外设角色,会向外界发出广播,这个时候可以被多个中心角色发现,一旦外设角色被某个中心角色链接后,外设角色就会停止广播,其他中心角色就无法在链接到这个外设角色。中心角色可以扫描外设角色,可以监听接收广播或主动链接,一个中心角色可以与多个外设同时链接。

Android蓝牙开发

首先在AndroidManifest.xml中加入权限

1
2
3
4
<uses-permissionandroid:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permissionandroid:name="android.permission.BLUETOOTH" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
<!-- 当android:required为true的时候,app只能强制运行在支持BLE功能的设备商,为false的时候,可以运行在所有设备上 -->

常用类

  • BluetoothAdapter 蓝牙适配器,可以通过它进行基础的蓝牙任务
    1. 初始化设备发现
    2. 获得配对的设备列表
    3. 使用MAC初始化蓝牙设备
    4. 初始化Socket监听蓝牙连接
    5. 扫描BLE设备等
  • BluetoothDevice 蓝牙设备
  • BluetoothServerSocket 蓝牙服务端Socket,类似TCP,服务端使用一个BluetoothServerSocket监听连接请求,接受连接后返回一个新的BluetoothSocket用于通讯,客户端使用一个单独的BluetoothSocket进行连接,最常见的蓝牙Socket是RFCOMM,是Android支持的,RFCOMM是一个蓝牙上面向连接的流传输,也被称为SPP
  • BluetoothSocket 蓝牙客户端Socket

基础开发流程

获得蓝牙适配器

API17及以下

1
BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();

API17以上

1
2
BluetoothManager bm = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter mAdapter = bm.getAdapter();
打开蓝牙
1
2
3
4
5
6
7
if (!mBluetoothAdapter.isEnabled()) {
// 跳转到系统设置打开
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent, REQUEST_ENABLE_BLUETOOTH);
// 强行打开
// mAdapter.enable()
}
获取配对设备
1
Set<BluetoothDevice> devices = mBluetoothAdapter.getBondedDevices();
搜索设备

搜索设备得到的结果会通过广播通知,所以先注册广播接收器,然后开始搜索

1
2
3
4
5
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_FOUND);
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
getActivity().registerReceiver(mBluetoothReceiver, filter);
1
mBluetoothAdapter.startDiscovery();
配对
1
2
3
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
btDev.createBond();
}

同样在广播中检测配对状态

建立连接

建立连接会阻塞线程,需要在新线程中进行

服务端和客户端需要指定相同的UUID才能连接

服务端

1
2
BluetoothServerSocket serverSocket = mAdapter.listenUsingRfcommWithServiceRecord(serverSocketName, UUID);
serverSocket.accept();

客户端

1
2
BluetoothSocket clienSocket = dcvice.createRfcommSocketToServiceRecord(UUID);
clienSocket.connect();
数据传输

获取输入输出流

1
2
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
其他
1
2
3
4
5
6
7
8
if (mAdapter.getProfileConnectionState(BluetoothProfile.A2DP) == BluetoothProfile.STATE_CONNECTED) {
mAdapter.getProfileProxy(context, new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
List<BluetoothDevice> devicesList = proxy.getConnectedDevices();
}
});
}

getProfileConnectionState()获得某种特定协议的连接状态,比如判断是不是有蓝牙音箱设备连接

getProfileProxy()获得协议的代理对象

BluetoothProfile.getConnectedDevices()获得该协议连接的设备

BLE开发

检查是否支持BLE
1
getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);

对于6.0以上,需要动态申请一个定位权限

1
2
3
4
5
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (this.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1);
}
}
扫描设备

通用的搜索方法可以搜索所有的蓝牙设备,这里只扫描BLE设备

1
2
3
4
5
6
private BluetoothAdapter.LeScanCallback mLeScanCallback= new BluetoothAdapter.LeScanCallback(){
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
// 不能执行耗时操作,不宜执行复杂运算操作
}
};

这是个过时的方法,但仍可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
}

@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
}

@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
}
};

mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback);

所有搜索都会对流畅性有影响,都需要在子线程进行
Android4.3开始支持BLE,Android5.0开始支持手机端作为从设备

连接BLE设备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
private BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
//当设备与中心连接状态发生改变时
switch (status) {
case BluetoothProfile.STATE_CONNECTED:
//这里表示已经成功连接,如果成功连接,就去发现设备所包含的服务
gatt.discoverServices();
break;
case BluetoothProfile.STATE_DISCONNECTED:
//表示GATT连接已经断开。
break;
}
}

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
//当发现设备服务时,会回调到此处
if (status == BluetoothGatt.GATT_SUCCESS) {
// 获得并遍历服务
for (BluetoothGattService service : gatt.getServices()) {
// 获得并遍历特征
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
// 下面是一个例子 使能从设备的通知 并拿到某个写通道
if (character.getUuid() != null) {
if (character.getUuid().toString().startsWith("0000fec8")) {
// 允许notification
mBluetoothGatt.setCharacteristicNotification(character, true);
BluetoothGattDescriptor descriptor = character.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
// 允许indication
descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
} else if (character.getUuid().toString().startsWith("0000fec7")) {
mWriteCharacter = character;
}
}
}
}
}
//当方法执行完后,我们就获取了设备所有的特征了。
}

@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
//读取特征后回调到此处
}

@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
//写入特征后回调到此处
}

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
//当特征(值)发生变化时回调到此处
}

@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorRead(gatt, descriptor, status);
//读取描述符后回调到此处
}

@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorWrite(gatt, descriptor, status);
//写入描述符后回调到此处
}

@Override
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
super.onReliableWriteCompleted(gatt, status);
}

@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
super.onReadRemoteRssi(gatt, rssi, status);
//RSSI表示设备与中心的信号强度,发生变化时回调到此处
}

@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
}
};

BluetoothGatt mBluetoothGatt = device.connectGatt(BluetoothLeService.this, false, mBluetoothGattCallback);
数据交互
1
2
3
4
5
6
7
8
9
10
11
//读取特征,相当简单,读取结果会回调到mGattCallback中的onCharacteristicRead。
private void readCharacteristic(BluetoothGattCharacteristic characteristic){
mBluetoothGatt.readCharacteristic(characteristic);
}

//写入特征,也相当简单,写入结果会回调到mGattCallback中的onCharacteristicWrite
//一次传输20字节,但貌似不需要做手动处理
private void readCharacteristic(BluetoothGattCharacteristic characteristic){
characteristic.setValue(value);
mBluetoothGatt.writeCharacteristic(characteristic);
}

系统回调BluetoothGattCallback.onCharacteristicWrite()方法通知数据已经完成写入。此时,我们需要执行BluetoothGattCharactristic.getValue()方法检查一下写入的数据是否我们需要发送的数据,如果不是按照项目的需要判断是否需要重发

蓝牙的写入操作,取操作必须序列化进行。写入数据和读取数据是不能同时进行的,如果调用了写入数据的方法,马上调用写入数据或者读取数据的方法,第二次调用的方法会立即返回 false, 代表当前无法进行操作. 断开连接

BLE蓝牙连接断开非常重要,因为主设备支持的设备数量有限,如果没有释放,后面连接可能总是失败

当蓝牙成功连接之后,通过BluetoothGatt.disconnect()断开蓝牙的连接,紧接着在 BluetoothGattCallback.onConnectionStateChange() 执行BluetoothGatt.close() 方法释放资源

分享