快速上手 Android 蓝牙串口 SPP 开发

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

访问我的独立博客页下载预编译文件(在最下面)

大家有缘江湖再见

江湖再见表情包
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342