ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

安卓BLE蓝牙串口助手源码解析:从GATT到稳定收发

安卓BLE蓝牙串口助手源码解析:从GATT到稳定收发 简介一套基于 Android Studio 的 BLE 蓝牙串口助手完整工程源码面向移动开发者、物联网爱好者以及需要调试智能硬件的人群。工程实现了蓝牙 4.0 低功耗通信的完整链路涵盖设备扫描、GATT 连接、服务发现、特征值读写与通知回调并在 MainActivity、BluetoothLeService、GattCallback 等类中展示了清晰的分层结构便于直接运行或移植到实际项目中。压缩包共 1946 个文件约 27.69MB包含 719 个 XML 界面与配置、618 个 PNG 图标资源、32 个 JAR 依赖库、13 个 Java 源文件及 APK 安装包与 Gradle 构建脚本统观类型即可判断这是一份可编译调试的完整工程。目前已有 1423 人学习下载。通过研读这套源码既能掌握 Android 端 BLE 串口通信的权限适配、连接稳定性与功耗控制要点也能获得一套可直接改造的通信框架特别适合课程设计、毕业设计或产品原型的起步参考。 搞安卓开发这些年经常要跟嵌入式设备打交道。尤其是做智能硬件、机器人、环境监测这类项目时最离不开的就是“手机蓝牙收数据”这个动作。市面上的蓝牙串口助手不少但真正能用、能改、能嵌到自己工程里的其实不多。看到 BLE_SPP 安卓手机蓝牙串口助手源码 这个名字我第一反应是这正好戳中了嵌入式调试和安卓联调的一大痛点。这篇文章我就顺着这个项目把 BLE_SPP 到底是什么、源码该怎么拆、接入时有哪些坑一次性说清楚。先说结论BLE_SPP 不是传统意义上的蓝牙 SPP串行端口协议而是基于 BLE低功耗蓝牙的 GATT 协议在逻辑上“模拟”出一个串口通道。它解决的典型问题是手机端做一个上位机实时接收 MCU、传感器、蓝牙模块发来的数据同时也能向下发指令。适合做嵌入式开发联调、外设控制、数据采集展示也适合刚入门蓝牙开发的安卓同学拿来当学习样板。1. 出发点为什么蓝牙串口助手要选 BLE_SPP 这条路1.1 串口调试的线缆困局做嵌入式开发的人都清楚调试阶段最烦的不是代码而是那根串口线。USB 转 TTL 的线一插电脑端开个串口助手看起来稳定但只要设备一挪动、一装箱、一上电线就掉了。更麻烦的是很多设备是电池供电的没有独立电源地线插上电脑调试时电平参考不一致稍不留神就是乱码甚至烧串口。蓝牙串口助手的出现本质上是把“物理串口线”换成“无线虚拟串口”。手机当成上位机模块端则通过蓝牙透传和 MCU 的 UART 对接。这样调试时不用再蹲在设备旁边尤其是跑小车、调电机、看传感器实时曲线的时候体验完全不同。1.2 经典蓝牙 SPP 与 BLE 的取舍一听“SPP”做过蓝牙开发的老哥可能会想起经典蓝牙的 BluetoothSocket。但安卓从 4.3 开始全面支持 BLE 之后新设备上做串口透传我个人的建议是别再死磕经典蓝牙了。经典蓝牙 SPP 的问题是系统层限制多连接前必须配对配对弹窗、权限、发现流程都很繁琐。而且现在很多低成本的 BLE 模块只支持低功耗蓝牙根本没开经典蓝牙那一路射频。反过来BLE 的优势非常明显功耗低、连接快、不需要显式配对、安卓手机兼容性好。唯一要适应的是把“流式写入”的心智模型转换成“特征值读写 通知”的模型。1.3 BLE_SPP 到底解决了什么问题BLE_SPP 这个项目实际上就是做了一层“协议转换壳”硬件上MCU 的串口数据通过 BLE 模块的 UART 透传出去软件上手机 App 封装 GATT 客户端逻辑把收到的数据拼成串口流把用户输入的命令拆包发下去。对使用源码的人来说它最大的价值不是“能连接”这三个字而是背后的一整套处理逻辑扫描过滤、连接状态机、MTU 协商、分包发送、粘包重组、异常重连。如果没有这套逻辑你写一个 demo 连上蓝牙模块很容易但要做到“稳定地收发数据”没有一个月踩坑是写不顺的。2. 核心源码架构先看懂 GATT 这盘棋2.1 BLE 串口的本质用 GATT 模拟串口BLE 没有像经典蓝牙那样现成的串口 Profile所以大家约定俗成在 GATT 服务里自定义一个“UART 服务”通常包含三个特征值写特征Write手机发给设备的数据通道。通知特征Notify设备发给手机的数据通道需要先开启通知才能接收。读特征Read用来读一些静态状态部分实现会省略。一般大家会直接采用北欧半导体Nordic定义的 UART Service UUID或者用厂商自定义的 FFE0/FFE1。无论用什么 UUID对于安卓客户端来说核心工作是一样的拿到服务找到对应的特征值然后读写。我给个配置示例object BleUartProfile { // 以 Nordic UART Service 为例 val SERVICE_UUID UUID.fromString(6e400001-b5a3-f393-e0a9-e50e24dcca9e) val WRITE_UUID UUID.fromString(6e400002-b5a3-f393-e0a9-e50e24dcca9e) val NOTIFY_UUID UUID.fromString(6e400003-b5a3-f393-e0a9-e50e24dcca9e) }2.2 服务与特征声明一套可以拿来即用的 UUID用这套 UUID 有个好处市面上的 BLE 透传模块很多出厂默认就广播这个服务。比如常见的 JDY 系列、HM-10 系列的 AT 透传模式连上后就能用。但如果你手里模块的服务是自定义的比如很多国产模块的默认服务 UUID 是FFE0特征值是FFE1那你只需要改前面那个BleUartProfile对象里的三个 UUID 即可。源码里把这一层抽出来后面换任何模块都只动一处不用满屏去改常量。这是初学者最容易忽略的设计UUID 必须集中管理。2.3 数据收发链路从手机到 MCU 的完整路径整个收发链路可以理解为三层物理链路层BLE 射频传输每次最多传“MTU-3”字节。逻辑链路层利用 GATT 的 Write 和 Notify 特征完成双向数据交互。应用协议层对字节流进行分帧、组包、校验。打个比方BLE 就像一条限宽的乡间小路你有一卡车数据不能一次全倒过去得把货分成小箱子一箱箱运。MTU 协商就是决定箱子能装多大应用协议层就是决定箱子外面贴什么标签、收到之后怎么按顺序装回来。源码里不出意外会在这层写一个数据缓冲池把onCharacteristicChanged回调里的原始字节攒起来再按自定义协议吐给上层业务。3. 从零到上手安卓端接入实操与关键代码3.1 工程配置权限、SDK 版本与依赖这一步是最容易出幺蛾子的尤其是安卓 12 之后权限策略大改很多老代码直接崩。用这份源码时请对照下面的配置清单核对。先在AndroidManifest.xml里加权限uses-permission android:nameandroid.permission.BLUETOOTH android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_SCAN / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION /这里特别提醒三点安卓 6.0 到 11.0扫描蓝牙必须开定位权限而且是运行时权限不是清单里写一下就完事。安卓 12API 31及以上必须用BLUETOOTH_SCAN和BLUETOOTH_CONNECT并且要用requestPermissions动态申请。如果你的项目targetSdk是 30 及以下不要在旧手机上声明BLUETOOTH_SCAN否则系统会直接忽略这个权限还可能引发问题。build.gradle里的关键配置我建议这样android { compileSdk 34 defaultConfig { minSdk 21 targetSdk 34 } }minSdk 21覆盖了绝大多数安卓设备如果你只需要在近几年的手机上跑也可以直接minSdk 23省去动态申请定位权限的一部分兼容性判断。3.2 核心流程扫描→连接→发现服务→收发数据BLE 客户端的标准流程就是四步扫描、连接、发现服务、收发数据。源码里通常会把这几步封装在一个BluetoothLeClient类里我直接讲关键调用点。扫描要设置合理的过滤条件不然会扫出一堆无关设备。建议按服务 UUID 过滤也就是只显示广播了SERVICE_UUID的设备val scanFilter ScanFilter.Builder() .setServiceUuid(ParcelUuid(BleUartProfile.SERVICE_UUID)) .build() val settings ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build() bluetoothLeScanner.startScan(listOf(scanFilter), settings, scanCallback)扫描到目标设备后立刻停止扫描然后发起连接。连接要用BluetoothGatt的connectGatt方法注意传autoConnectfalse这样连接失败会快速回调方便做超时判断。连接成功后在onServicesDiscovered回调里拿到 UART 服务同时开启通知val gattService gatt.getService(BleUartProfile.SERVICE_UUID) val notifyChar gattService.getCharacteristic(BleUartProfile.NOTIFY_UUID) gatt.setCharacteristicNotification(notifyChar, true) val descriptor notifyChar.getDescriptor(UUID.fromString(00002902-0000-1000-8000-00805f9b34fb)) descriptor.value BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE gatt.writeDescriptor(descriptor)开通知这一步很多人容易漏掉 descriptor 的写入。光调setCharacteristicNotification(true)只是本地启用了回调服务端的通知开关是通过写0x0001到 CCCD客户端特征配置描述符来打开的。漏掉这步你就会发现设备明明在发数据手机端却一点动静都没有。发送数据时老的写法是gatt.writeCharacteristic(characteristic)但 API 33 之后推荐用writeCharacteristic(characteristic, value, WRITE_TYPE_DEFAULT)。3.3 大包传输MTU 协商与分帧组包发送数据最常见的坑是“超过单包长度直接丢”。BLE 在未协商 MTU 时默认的有效数据长度是 20 字节ATT 层默认 MTU 23减 3 字节 ATT 头。如果一次写入超过 20 字节部分模块会报错部分模块直接丢弃。协商 MTU 的办法是gatt.requestMtu(247)然后在onMtuChanged回调里拿到实际协商后的 MTU。注意这个值不是你说 247 就一定是 247要看手机蓝牙协议栈和从机模块的支持上限实际值以回调里的mtu参数为准。拿到 MTU 后单包能传的有效字节数就是mtu - 3。比如协商结果是 247每次最多写 244 字节数据。发送大数据时自己写一个发送队列按mtu - 3切片每片之间要有 20ms 左右的间隔防止手机蓝牙缓冲溢出。很多从机模块的串口波特率不高发送太快会在模块端把数据挤丢。我一般会维护一个ArrayDequeByteArray一帧一帧地吐。3.4 通信协议的防坑设计BLE 传输本质是数据包不是流。如果只发一堆裸字节接收端没法判断一句命令在哪结束。所以我强烈建议源码里一定要有简单的分帧协议。最保险的格式是帧头 数据长度 数据区 校验帧头可以自定义比如取0xAA 0x55长度字段占 1~2 字节数据区按长度读取最后加一个累加和校验。安卓端收到数据后先判断帧头再按长度收满再做校验校验通过才交给上层解析。这能挡住 90% 以上的粘包/拆包/错位问题。4. 实机验证与调参心得4.1 不同安卓手机的兼容性表现源码写完之后我会拿至少三台不同系统版本的手机实机测分别是一台老旧的安卓 8 手机、一台主流安卓 11 手机、一台最新的安卓 14 手机。实测下来有几个规律老手机扫描慢有时要扫三到五秒才能发现设备可以适当延长扫描超时。部分国产 ROM 在系统级别对后台蓝牙扫描做了限制App 退到后台后回调会被挂起。所以要长连调试建议把连接过程放在前台 Service 里并加一个常驻通知。同一台手机第一次连接设备成功断开后马上重连可能失败原因是系统缓存了旧的 GATT 信息。遇到这种情况先调gatt.refresh()反射调用清一下缓存再重新连接成功率会高很多。4.2 吞吐与耗电平衡BLE 本身是低功耗协议但如果代码写得粗糙手机端多耗的电比其他无线方案还猛。几个调参重点扫描模式用SCAN_MODE_LOW_LATENCY仅在扫描阶段用连接成功后马上停。数据接收频率高时不要把 UI 刷新和数据解析放在同一个线程。用Handler或协程切线程避免回调阻塞。如果只需要定时采样比如每秒钟读一次传感器建议在下位机做定时上报而不是手机端轮询读取这样手机和模块都省电。我实测过一个方案MTU 协商到 247单帧 244 字节、间隔 20ms 发送手机端实时显示和存储连续跑两个小时没有掉包。吞吐大概能到 10KB/s 左右对绝大多数传感器数据上报场景完全够用。5. 常见问题速查与排错实录很多人拿到源码跑不通问题往往不在源码本身而在环境。我把实际联调中最常碰到的几类问题整理成一张表方便你快速定位。现象可能原因排查思路扫描不到设备没开定位权限或扫描过滤条件不对确认权限已授予先用无过滤扫描看模块是否广播正常连接后马上断开模块进入了休眠或服务 UUID 不匹配查看连接回调的错误码用 nRF Connect 交叉验证能连接但收不到数据没有写 CCCD descriptor检查是否调用了writeDescriptor(ENABLE_NOTIFICATION_VALUE)发送没反应数据超过单包 MTU确认 MTU 协商结果按mtu-3分包发送数据乱码波特率不匹配检查串口模块端的波特率BLE 透传模块发送侧波特率一般要一致偶发断连系统蓝牙缓存异常反射调用refresh()清缓存后重连安卓 13 以上崩溃缺少BLUETOOTH_CONNECT权限检查运行时权限申请权限被拒绝要引导到设置页问得比较多的还有一个为什么用 nRF Connect 能连上自己的 App 连不上。这大概率是 UUID 写错了或者权限不全。nRF Connect 是个很强大的通用 BLE 调试工具遇到问题先用它验证模块的广播和服务再用它和你的 App 做交叉对比能省掉非常多排查时间。断线重连这块源码如果没有内置我建议至少要做到在onConnectionStateChange里监听断开状态断开后用指数退避重连重连前先扫描一次再连目标 MAC。重连次数不要无限五次失败后给用户弹提示避免后台无限重试把手机电量耗干。我个人在实际调试中还有个习惯在 App 里加一个“原始数据日志”的开关。打开后把onCharacteristicChanged收到的每个字节都按 HEX 格式打出来。很多时候看起来是数据不对其实是设备端根本没发或者帧结构不对有原始日志一眼就能定位问题。这个习惯帮我省了无数次来回改固件的折腾。如果后面时间充裕我会把这份源码再扩展一版加上数据库缓存和历史曲线展示做成通用调试平台。目前这套 BLE_SPP 方案其实已经能覆盖我绝大部分嵌入式联调场景了。希望这篇文章能帮你少走点弯路。本文还有配套的精品资源点击获取
返回列表