*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
0 前言
最近在接毕设当零花钱,要做物联网的比较多,经常需要用到蓝牙串口来和单片机通讯。引出了几个问题:
蓝牙串口是什么?
如何扫描蓝牙设备
如何连接蓝牙设备
如何收发串口数据
1 蓝牙串口是什么?
先介绍下串口,串行接口简称串口,就是一种通信的方式,类似于「USB」,只是比 USB 低级多了。但是手机等设备他没外置这个串口,解决方式就是手机用蓝牙连接一个小硬件,小硬件有个串口,他的和单片机连接,来达到手机和单片机的串口连接,这种方式就是蓝牙串口。
在开发之前你最好有那个小硬件,那个小硬件通常叫「蓝牙透传模块」,淘宝不到 30 块钱就能买一个。我的长这样,有专用的上位机,这个你不明的请联系卖你模块的人,他会给予技术支持的:
你要做的就是打开电脑上蓝牙模块的上位机的串口界面,能正常的收发数据即可:
2 如何扫描蓝牙设备
你肯定是有个问题,为啥不直接连接,而是要扫描呢?
因为连接需要使用「BluetoothDevice」,这个东西要么搜索到,要么用 「MAC」地址构造。「MAC」地址是每个设备独一无二的,所以必须要扫描设备,获取周围所有的设备列表,拿到 「BluetoothDevice」来连接。同时取出里面的 「MAC」地址,保存,用来下次连接。
我们先获取系统的蓝牙适配器,所有的搜索,连接,等操作都要靠他:
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
然后判断下用户的蓝牙是否已经开启:
/**
* 获取用户是否打开了蓝牙
*/
boolean isBluetoothEnable() {
return mBluetoothAdapter.isEnabled();
}
要是一个没开蓝牙就想连蓝牙的人,我们就勉为其难帮他开启下吧 #(笑
/**
* 开启蓝牙
*/
void enableBluetooth() {
mBluetoothAdapter.enable();
}
现在蓝牙已经开启了,那就开始搜索设备列表
mBluetoothAdapter.startDiscovery();
但是我们还需要考虑下是不是已经正在搜索:
mBluetoothAdapter.isDiscovering()
如果正在搜索就给他取消掉:
mBluetoothAdapter.cancelDiscovery()
所以结合起来就是:
/**
* 开始搜索
*/
void startDiscovery() {
if (mBluetoothAdapter.isDiscovering()) mBluetoothAdapter.cancelDiscovery();
mBluetoothAdapter.startDiscovery();
}
似乎出现了一个问题,结果在哪获取?
说出来你可能不信,用广播,你没听错,就是广播,别无他法 ( ´・・)ノ(._.`) ,当然也可能是我太菜了
先定义一个广播接收器,获取到搜索结果的 Action 是 BluetoothDevice.ACTION_FOUND
,然后在里面取出 BluetoothDevice.EXTRA_DEVICE
就可以获取到可爱的 BluetoothDevice
了。
还记的前文说的连接蓝牙需要的东西吗?就是他了
/**
* 搜索到新设备广播广播接收器
*/
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);
}
}
};
然后用 context
注册这个广播
IntentFilter foundFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
mContext.registerReceiver(mReceiver, foundFilter);
最后不要忘记加入权限,处理好运行时权限:
<!--管理蓝牙需要-->
<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 地址:
bluetoothDevice.getAddress()
把他保存下来,下次使用的时候就可以用它二次获取 BluetoothDevice
了
bluetoothDevice = bluetoothAdapter.getRemoteDevice("之前保存过的蓝牙MAC地址");
到此搜索的部分就结束了
3 如何连接蓝牙设备
上一节说道,拿到了 BluetoothDevice
就可以用来连接了,连接很简单,首先要知道每个蓝牙设备都有一个 UUID 来描述自己是什么设备,蓝牙串口设备的缩写是 SPP,他的 UUID 如下,其他的 UUID 详情,可以参考这个页面
UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
然后用之上一步拿到的 BluetoothDevice
来打开指定 UUID 的连接即可获取到蓝牙的 Socket,要注意,只能和 UUID 类型对应的设备连接,比如我们这里设置的 UUID 是 SPP 的,和普通的手机就连不上
BluetoothSocket bluetoothSocket = bluetoothDevice.createRfcommSocketToServiceRecord(SPP_UUID);
现在到了激动人心的时刻,建立连接!
调用 bluetoothSocket
的 connect()
方法会建立和蓝牙模块的连接,如果之前没有配对过,会弹出系统窗口,要求用户输入配对密码,这一块开发者没有特殊需求不用关心,系统会自动处理。要注意的是 connect 方法会阻塞线程,需要在子线程建立连接:
// 等待连接,会阻塞线程
bluetoothSocket.connect();
然后通过 BluetoothSocket
即可拿到输入流和输出流:
// 用来收数据
InputStream inputStream = bluetoothSocket.getInputStream();
// 用来发数据
OutputStream outputStream = bluetoothSocket.getOutputStream();
这就是普通的流操作了,就是大家熟悉的内容了
4 如何收发串口数据
发数据就是传统的流操作了,调用 OutputStream
的 write(byte[])
方法来写入流:
/**
* 发送
*
* @param msg 内容
*/
void send(byte[] msg) {
try {
bluetoothSocket.getOutputStream().write(msg);
} catch (Exception e){e.printStackTrace();}
}
收数据需要注意一下,需要写个死循环,反复读取,因为串口发来的一句话很可能是分成好几段发来的,和单片机那边的开发约定好一个停止位,没收到停止位之前就一直累加,这里给出一个我调试好的模板代码:
// 记录标志位,开始运行
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 总结与结语
到此,就算是大体结束了,但是不要忘记关闭线程,关闭流,解注册广播等等,我包装了一个工具类,上面的具体连贯实现也可以参考。这个工具类可以在这里获取
这个项目有个 demo,是个串口演示,可以在 Github 获取
大家有缘江湖再见