Android蓝牙串口SPP开发

原文地址:https://blog.gtf35.top/bluetooth_spp/

【这里仅供学习】

0. 前言

物联网形势下,经常需要用到蓝牙串口来和单片机通讯。问题:

  • 蓝牙串口是什么?
  • 如何扫描蓝牙设备?
  • 如果连接蓝牙设备?
  • 如果收发串口数据?

1. 蓝牙串口是什么?

先介绍一下串口,串行接口简称串口,就是一种通信的方式,类似于USB,只是比USB低级多。但是手机等设备没有外设这个串口,解决方案就是用手机蓝牙模块连接一个小硬件,小硬件有个串口,可以和单片机连接,来达到手机和单片机的串口连接,这种方式就是蓝牙串口。

那个小硬件就是“蓝牙透传模块”,淘宝上有卖,有专用的上位机。

这里要做的就是打开电脑上蓝牙模块的上位机的串口界面,能正常收发数据即可:

2. 如何扫描蓝牙设备?

为什么不直接连接,而是扫描呢?

因为连接需要使用BluetoothDevice,这个东西要么搜索到,要么使用“MAC”地址构造。“MAC”地址是每台设备独一无二的,所以必须要扫描设备,获取周围所有的设备列表,拿到BluetoothDevice来连接。同时取出里面的“MAC”地址,保存,用于下次连接。

首先获取系统的蓝牙适配器,所有的搜索,连接,等操作都要靠它:

1
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

然后判断用户的蓝牙是否已开启:

1
2
3
4
5
6
/**
* 获取用户是否打开了蓝牙
*/
boolean isBluetoothEnable() {
return mBluetoothAdapter.isEnabled();
}

开启蓝牙:

1
2
3
4
5
6
/**
* 开启蓝牙
*/
void enableBluetooth() {
mBluetoothAdapter.enable();
}

蓝牙开启之后,就可以搜索设备列表:

1
mBluetoothAdapter.startDiscovery();

搜索前,还需要判断是不是已经在搜索了:

1
mBluetoothAdapter.isDiscovering();

如果正在搜索,就取消搜索:

1
mBluetoothAdapter.cancelDiscovery();

结合起来就是:

1
2
3
4
5
6
7
/**
* 开始搜索
*/
void startDiscovery() {
if (mBluetoothAdapter.isDiscovering()) mBluetoothAdapter.cancelDiscovery();
mBluetoothAdapter.startDiscovery();
}

那么,结果在哪获取?可以通过广播。

先定义一个广播接收器,获取搜索结果的ActionBluetoothDevice.ACTION_FOUND,然后在里面取出BluetoothDevice.EXTRA_DEVICE,就可以获得BluetoothDevice了。

自定义广播:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 搜索到新设备广播广播接收器
*/
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// 这就是可爱的 BluetoothDevice 了
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
}
}
};

注册广播:

1
2
IntentFilter foundFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
mContext.registerReceiver(mReceiver, foundFilter);

添加权限:

1
2
3
4
5
6
<!--管理蓝牙需要-->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!--搜索蓝牙需要,因为蓝牙可以被用来定位,所以需要定位权限-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

这样在触发搜索逻辑之后,每次找到一个新设备就会收到一个广播,拿到BluetoothDevice之后,就可以获取“MAC“地址:

1
bluetoothDevice.getAddress();

保存下来,下次使用的时候就可以用它二次获取BluetoothDevice了:

1
bluetoothDevice = bluetoothAdapter.getRemoteDevice("之前保存过的蓝牙MAC地址");

到这里,搜索部分结束。

3. 如何连接蓝牙设备

前面说到,拿到BluetoothDevice就可以用来连接了,连接很简单,首先要知道每个蓝牙设备都有一个UUID来描述自己是什么设备,蓝牙串口设备到的缩写是SPP,它的UUID如下,其他的UUID详情,可以参考这里

1
UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");

然后用前面拿到的BluetoothDevice来打开指定UUID的连接即可获取到蓝牙的Socket,注意,只能和UUID类型对应的设备连接,比如这里设置的UUID是SPP的,和普通的手机就连不上:

1
BluetoothSocket bluetoothSocket = bluetoothDevice.createRfcommSocketToServiceRecord(SPP_UUID);

调用BluetoothSocketconnect()方法建立和蓝牙模块的连接,如果之前没有配对过,会弹出系统窗口,要求输入配对密码,这里系统会自动处理。要注意connect()方法会阻塞线程,需要在子线程连接:

1
2
// 等待连接,会阻塞线程
bluetoothSocket.connect();

然后通过BluetoothSocket即可拿到输入流和输出流:

1
2
3
4
// 用来收数据
InputStream inputStream = bluetoothSocket.getInputStream();
// 用来发数据
OutputStream outputStream = bluetoothSocket.getOutputStream();

4. 如何收发串口数据

发数据就是传统的流操作,调用OutputStreamwrite(byte[])方法来写入流:

1
2
3
4
5
6
7
8
9
10
/**
* 发送
*
* @param msg 内容
*/
void send(byte[] msg) {
try {
bluetoothSocket.getOutputStream().write(msg);
} catch (Exception e){e.printStackTrace();}
}

收数据需要注意,需要写一个死循环,反复读取,因为串口发来的一句话很可能是分成好几段发来的,和单片机的开发约定好一个停止位,没收到停止位之前,就一直累加,这里给出一个调试好的模板代码:

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
// 记录标志位,开始运行
boolean isRunning = true;
// 约定好的停止位
String stopString = "\r\n";

// 开始监听数据接收
try {
InputStream inputStream = bluetoothSocket.getInputStream();
byte[] result = new byte[0];
while (isRunning) {
logD("looping");
byte[] buffer = new byte[256];
// 等待有数据
while (inputStream.available() == 0 && isRunning) {if (System.currentTimeMillis() < 0) break;}
while (isRunning) {
try {
int num = inputStream.read(buffer);
byte[] temp = new byte[result.length + num];
System.arraycopy(result, 0, temp, 0, result.length);
System.arraycopy(buffer, 0, temp, result.length, num);
result = temp;
if (inputStream.available() == 0) break;
} catch (Exception e) {
e.printStackTrace();
// todo:处理接收数据单次失败
break;
}
}
try {
// 返回数据
logD("当前累计收到的数据=>" + byte2Hex(result));
byte[] stopFlag = stopString.getBytes();
int stopFlagSize = stopFlag.length;
boolean shouldCallOnReceiveBytes = false;
logD("标志位为:" + byte2Hex(stopFlag));
for (int i = stopFlagSize - 1; i >= 0; i--) {
int indexInResult = result.length - (stopFlagSize - i);
if (indexInResult >= result.length || indexInResult < 0) {
shouldCallOnReceiveBytes = false;
logD("收到的数据比停止字符串短");
break;
}
if (stopFlag[i] == result[indexInResult]) {
logD("发现" + byte2Hex(stopFlag[i]) + "等于" + byte2Hex(result[indexInResult]));
shouldCallOnReceiveBytes = true;
} else {
logD("发现" + byte2Hex(stopFlag[i]) + "不等于" + byte2Hex(result[indexInResult]));
shouldCallOnReceiveBytes = false;
}
}
if (shouldCallOnReceiveBytes) {
// 到了这里,byte 数组 result 就是收到的数据了
// todo: 执行收到数据逻辑
// 清空之前的
result = new byte[0];
}
} catch (Exception e) {
e.printStackTrace();
// todo:处理验证收到数据结束标志出错
}
}
} catch (Exception e) {
e.printStackTrace();
// todo:处理接收数据失败
}

5. 总结

主要流程基本结束,后面不要忘记关闭线程,关闭流,解除注册广播等。

源码地址:BluetoothLowEnergyDemo

  • Copyrights © 2019-2020 Tyler Liu

请我喝杯咖啡吧~

支付宝
微信