登录后台

页面导航

本文编写于 119 天前,最后修改于 116 天前,其中某些信息可能已经过时。

*本篇文章已授权微信公众号 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);

现在到了激动人心的时刻,建立连接!

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

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

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

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

这就是普通的流操作了,就是大家熟悉的内容了

4 如何收发串口数据

发数据就是传统的流操作了,调用 OutputStreamwrite(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 获取

点击此处下载编译好的 apk

大家有缘江湖再见

江湖再见表情包

已有 8 条评论