*本篇文章已授权微信公众号 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 获取
大家有缘江湖再见

read failed, socket might closed or timeout, read ret: -1,报这个错,是不是代码只能连蓝牙模块,连接不了手机蓝牙?都是蓝牙有啥区别?
who 2023-12-25 00:24
大佬,你好,如果手机蓝牙已经打开,会运行闪退卡死怎么解决?手机是android11.
youngyyzz 2021-11-13 02:54
下载 demo 运行就闪退,也没啥有用的报错信息,怎么破。。。 (ó﹏ò。)
Allen 2020-08-05 22:54
应该不会呀🤔🤔虽然没啥用还是贴一下崩溃信息吧,感谢
gtf35 2020-08-06 18:23
奇怪了,没有任何信息,Android 5 的两台设备都是这样
Allen 2020-08-06 18:36
大佬,请问你的android是用 studio 开发的还是eclipsek开发的?
gogo218 2020-06-26 22:31
很抱歉才看到你的评论。
「Eclipse」 已经不被官方支持了,使用「Android Studio」是趋势,我也是使用的「Android Studio」。它基于「IDEA」社区版开发,相信我,很智能的,用了离不开 :)
gtf35 2020-07-03 05:56
大佬你好,我买了一样的透传模块,用你的demo测试,报错read failed, socket might closed or timeout, read ret: -1,这个怎么解决呢?
路不离开 2020-06-04 18:15
检查下是不是连错了蓝牙模块,手机等别的蓝牙设备是无法连接的,详见下一条
检查下蓝牙模块是不是SPP的模式,BLE 的连不了,因为 UUID 不一样
检查下蓝牙模块是不是从机模式
检查下蓝牙模块是不是正常工作,尝试下复位
gtf35 2020-06-04 19:14
好吧,我搞错了,我的是BLE,谢谢大佬的回复
路不离开 2020-06-04 20:31