1. 充电桩Android App的BLE蓝牙模块开发实战
作为一名在Android蓝牙开发领域深耕多年的开发者,我最近完成了一个充电桩App的BLE蓝牙通信模块开发。这个项目让我深刻体会到,在IoT设备通信中,协议对齐和稳定性设计的重要性远超普通蓝牙应用。下面我将从协议实现、稳定性设计和实战经验三个维度,分享这个模块的开发细节。
1.1 项目背景与核心需求
充电桩与App的BLE通信不同于普通蓝牙设备,它需要:
- 严格遵循桩企私有协议(分包/合包、CRC校验、指令集)
- 适应Android各版本蓝牙权限差异
- 处理高频率心跳包和异常上报
- 确保配网过程的稳定性和容错性
我们的目标是开发一个能够满足这些严苛要求的BLE通信模块,同时保持代码的可维护性和可扩展性。
2. 协议核心实现
2.1 协议帧结构设计
充电桩BLE协议采用分层设计:
code复制[帧头2][版本1][帧类型1][数据长度2][指令码2][数据域N][CRC16 2][帧尾2]
这种设计既保证了协议的可扩展性,又便于校验和解析。
2.1.1 帧合法性校验
kotlin复制fun checkFrameValid(frame: ByteArray): Boolean {
if (frame.size < 10) {
LogUtil.e("帧长度过短:${frame.size}")
return false
}
// 帧头校验
val header = ByteBuffer.wrap(frame, 0, 2).short.toInt() and 0xFFFF
if (header != FRAME_HEADER) return false
// 版本校验
val version = frame[2].toInt() and 0xFF
if (version != PROTOCOL_VERSION) return false
// 帧尾校验
val tail = ByteBuffer.wrap(frame, frame.size - 2, 2).short.toInt() and 0xFFFF
return tail == FRAME_TAIL
}
注意:帧校验是协议处理的第一步,必须在校验通过后才能进行后续解析,否则可能引发安全问题和数据错乱。
2.2 CRC16校验实现
充电桩协议使用特殊的CRC16算法:
- 初始值:0xFFFF
- 多项式:0x8005
- 结果取反
- 小端序存储
kotlin复制fun calculateCRC16(data: ByteArray): ByteArray {
var crc = 0xFFFF
data.forEach { byte ->
crc = crc xor (byte.toInt() and 0xFF) shl 8
repeat(8) {
crc = if (crc and 0x8000 != 0) {
(crc shl 1) xor 0x8005
} else {
crc shl 1
}
}
}
crc = crc.inv() and 0xFFFF
return byteArrayOf(
(crc and 0xFF).toByte(),
(crc shr 8 and 0xFF).toByte()
)
}
踩坑记录:初期使用查表法实现CRC校验,结果与桩端不一致。后来发现协议要求逐位计算而非查表,修改后问题解决。这说明IoT设备通信必须严格遵循协议文档。
2.3 分包与合包机制
BLE单帧最大20字节,超长数据需要分包发送:
kotlin复制fun splitPackage(data: ByteArray): List<ByteArray> {
if (data.size <= BLE_MAX_FRAME_LEN) return listOf(data)
val totalPackage = (data.size + BLE_MAX_FRAME_LEN - 1) / BLE_MAX_FRAME_LEN
if (totalPackage > 15) {
LogUtil.e("分包数超过15,无法发送")
return emptyList()
}
return (0 until totalPackage).map { packageIndex ->
val head = (totalPackage shl 4) or packageIndex
val len = min(BLE_MAX_FRAME_LEN, data.size - packageIndex * BLE_MAX_FRAME_LEN)
ByteArray(len + 1).apply {
this[0] = head.toByte()
System.arraycopy(data, packageIndex * BLE_MAX_FRAME_LEN, this, 1, len)
}
}
}
合包处理需要考虑:
- 多设备同时通信时的缓存隔离
- 5秒超时丢弃机制
- 分包序号连续性检查
kotlin复制private val packageCache = HashMap<String, PackageCacheBean>()
fun mergePackage(deviceAddress: String, frame: ByteArray): ByteArray? {
val head = frame[0].toInt() and 0xFF
val totalPackage = head shr 4
val currentIndex = head and 0x0F
// 初始化缓存
var cache = packageCache[deviceAddress]
if (cache == null || cache.totalPackage != totalPackage) {
cache = PackageCacheBean(totalPackage)
packageCache[deviceAddress] = cache
// 启动超时定时器
CoroutineScope(Dispatchers.IO).launch {
delay(PACKAGE_MERGE_TIMEOUT)
if (packageCache[deviceAddress]?.totalPackage == totalPackage) {
LogUtil.w("合包超时,丢弃缓存:$deviceAddress")
clearCache(deviceAddress)
}
}
}
// 缓存分包
cache.addPackage(currentIndex, frame.copyOfRange(1, frame.size))
// 检查是否全部接收完成
return if (cache.isAllPackageCached()) {
val mergeData = cache.mergeAllPackage()
clearCache(deviceAddress)
mergeData
} else {
null
}
}
3. 稳定性设计
3.1 断连重连机制
充电桩场景中,蓝牙连接可能因各种原因中断。我们实现了三级重连策略:
- 立即重连:连接断开后立即尝试重连(间隔1秒)
- 退避重连:连续失败后采用指数退避策略(2秒、4秒、8秒...)
- 用户触发重连:超过最大重试次数后等待用户手动触发
kotlin复制private fun reconnect(device: BluetoothDevice) {
if (reconnectCount.get() >= maxReconnectCount) {
LogUtil.w("达到最大重连次数,停止自动重连")
return
}
CoroutineScope(Dispatchers.IO).launch {
val delayTime = 1000L * (1 shl reconnectCount.get())
delay(delayTime.coerceAtMost(8000)) // 最大间隔8秒
LogUtil.i("尝试第${reconnectCount.get() + 1}次重连...")
bluetoothGatt = device.connectGatt(context, false, gattCallback)
reconnectCount.incrementAndGet()
}
}
3.2 心跳保活机制
10秒一次的心跳包用于:
- 维持蓝牙连接
- 检测通信链路状态
- 上报设备信号强度
kotlin复制private var heartbeatJob: Job? = null
private fun startHeartbeat() {
heartbeatJob?.cancel()
heartbeatJob = CoroutineScope(Dispatchers.IO).launch {
while (true) {
delay(10000) // 10秒间隔
sendHeartbeat()
}
}
}
private fun sendHeartbeat() {
val frame = ProtocolFrameUtil.buildHeartbeatFrame()
sendData(frame) { success ->
if (!success) {
LogUtil.e("心跳发送失败,可能连接已断开")
}
}
}
经验:心跳超时3次判定为连接异常,应触发重连流程。同时要注意Android 6.0+的后台扫描限制。
3.3 数据发送重试
蓝牙数据传输可能因信号干扰失败,我们实现了三级重试:
kotlin复制fun sendData(data: ByteArray, callback: (Boolean) -> Unit) {
var retryCount = 0
fun doSend() {
if (retryCount >= 3) {
callback(false)
return
}
if (communicationChar == null) {
callback(false)
return
}
communicationChar!!.value = data
val success = bluetoothGatt?.writeCharacteristic(communicationChar!!) ?: false
if (!success) {
retryCount++
CoroutineScope(Dispatchers.IO).launch {
delay(500)
doSend()
}
} else {
callback(true)
}
}
doSend()
}
4. Android权限适配
4.1 版本差异处理
不同Android版本的蓝牙权限要求:
| Android版本 | 必需权限 |
|---|---|
| 4.3-10 | BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_COARSE_LOCATION |
| 10-11 | 增加ACCESS_FINE_LOCATION |
| 12+ | 用BLUETOOTH_SCAN/CONNECT替代传统权限 |
xml复制<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
4.2 运行时权限请求
kotlin复制fun checkPermissions(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
} else {
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
}
注意:Android 10+需要在定位权限被拒绝时解释用途,否则应用商店可能拒绝上架。
5. 核心指令实现
5.1 设备握手流程
握手是建立通信的第一步,包含设备信息交换:
kotlin复制fun handshake(appVersion: String, callback: (HandshakeResponse?) -> Unit) {
val frame = ProtocolFrameUtil.buildHandshakeFrame(appVersion)
sendData(frame) { success ->
if (!success) {
callback(null)
return@sendData
}
// 等待响应
responseHandler = { responseFrame ->
val response = ProtocolFrameUtil.parseHandshakeResponse(responseFrame)
if (response != null) {
deviceId = response.deviceId
callback(response)
} else {
callback(null)
}
}
}
}
5.2 WiFi配网流程
配网是充电桩App的核心功能,流程复杂:
- 获取WiFi列表
- 选择网络并发送配网请求
- 轮询配网状态
- 处理配网结果
kotlin复制fun startWifiConfig(ssid: String, password: String, timeout: Int) {
// 1. 发送配网请求
val frame = ProtocolFrameUtil.buildWifiConfigFrame(ssid, password, timeout)
sendData(frame) { success ->
if (!success) {
postConfigStatus(CONFIG_STATUS_FAILED, CONFIG_FAIL_WIFI_MODULE)
return@sendData
}
// 2. 启动状态轮询
configPollingJob = CoroutineScope(Dispatchers.IO).launch {
var elapsed = 0
while (elapsed < timeout * 1000) {
delay(2000) // 每2秒查询一次
queryConfigStatus()
elapsed += 2000
}
}
}
}
private fun queryConfigStatus() {
val frame = ProtocolFrameUtil.buildQueryConfigStatusFrame(taskId)
sendData(frame) { success ->
if (!success) return@sendData
responseHandler = { responseFrame ->
val status = ProtocolFrameUtil.parseConfigStatusResponse(responseFrame)
status?.let {
postConfigStatus(it.configStatus, it.failReason)
if (it.configStatus != CONFIG_STATUS_DOING) {
configPollingJob?.cancel()
}
}
}
}
}
6. 异常处理与调试
6.1 常见错误码处理
| 错误码 | 含义 | 处理建议 |
|---|---|---|
| 0x01 | 指令不支持 | 检查协议版本兼容性 |
| 0x02 | 参数格式错误 | 验证数据域编码 |
| 0x04 | CRC校验失败 | 检查CRC算法实现 |
| 0x05 | 验证码错误 | 重新发起握手 |
| 0x07 | 硬件故障 | 提示用户检查设备 |
kotlin复制fun handleErrorCode(code: Int): String {
return when (code) {
ERROR_CMD_NOT_SUPPORT -> "指令不支持,请升级App"
ERROR_PARAM_INVALID -> "参数格式错误"
ERROR_CRC16_FAILED -> "数据校验失败,请重试"
// ...其他错误码处理
else -> "未知错误(0x${Integer.toHexString(code)})"
}
}
6.2 日志封装
完善的日志系统是调试BLE的关键:
kotlin复制object LogUtil {
private const val TAG = "ChargingPile_BLE"
fun init(isDebug: Boolean) {
if (isDebug) {
Logger.init(TAG).setLogLevel(LogLevel.FULL)
} else {
Logger.init(TAG).setLogLevel(LogLevel.NONE)
}
}
fun d(msg: String) = Logger.d("$TAG: $msg")
fun i(msg: String) = Logger.i("$TAG: $msg")
fun w(msg: String) = Logger.w("$TAG: $msg")
fun e(msg: String, e: Throwable? = null) = Logger.e(e, "$TAG: $msg")
fun json(json: String) = Logger.json(json)
fun logFrame(direction: String, frame: ByteArray) {
if (!isDebug()) return
val hexStr = frame.joinToString(" ") { "%02X".format(it) }
d("$direction Frame: $hexStr")
}
}
6.3 常见问题排查
-
连接不稳定
- 检查设备距离(建议<5米)
- 避开2.4GHz WiFi干扰
- 禁用手机省电模式
-
配网失败
- 确认SSID/密码正确
- 检查桩端WiFi模块状态灯
- 尝试重置桩端网络配置
-
数据解析异常
- 确认字节序(小端序/大端序)
- 检查CRC计算方式
- 验证协议版本是否匹配
7. 性能优化建议
7.1 连接优化
- 快速连接:缓存设备GATT信息,减少服务发现时间
- 连接参数协商:根据场景调整连接间隔和延迟
kotlin复制fun updateConnectionPriority(priority: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
bluetoothGatt?.requestConnectionPriority(priority)
}
}
7.2 数据传输优化
- 数据压缩:对WiFi列表等大数据量场景使用压缩算法
- 批量写入:合并多个小数据包一次性发送
- 优先级队列:确保控制指令优先于状态查询
7.3 内存管理
- 及时释放资源:在onDestroy中释放GATT连接
- 避免内存泄漏:使用弱引用持有Context
- 对象复用:重用ByteBuffer等对象减少GC
8. 兼容性处理
8.1 厂商ROM适配
不同手机厂商的BLE实现存在差异:
- 小米:需关闭MIUI优化
- 华为:允许后台蓝牙扫描
- OPPO:关闭应用速冻
8.2 Android版本差异
- 4.x:BLE API不稳定,建议最低支持5.0+
- 6.0:需要动态位置权限
- 10:后台扫描限制
- 12:新增BLUETOOTH_SCAN/CONNECT权限
9. 测试方案
9.1 单元测试重点
- CRC16算法验证
- 分包/合包逻辑
- 协议帧构建与解析
- 错误码处理
9.2 集成测试场景
-
正常流程
- 连接→握手→心跳→控制指令
- WiFi扫描→配网→状态查询
-
异常场景
- 信号中断恢复
- 协议版本不匹配
- 非法数据帧注入
-
压力测试
- 连续100次配网操作
- 高频率指令发送(10次/秒)
- 大数据量传输(1KB+)
10. 项目总结
这个充电桩BLE模块的开发让我获得了三点重要经验:
-
协议先行:与硬件团队保持密切沟通,确保双方对协议理解完全一致。我们曾因CRC算法理解偏差导致两天调试无果。
-
防御性编程:IoT环境复杂,代码必须具备完��的错误检测和恢复能力。我们的重连机制在实际测试中成功处理了90%以上的偶发断连。
-
日志为王:完善的日志系统节省了大量调试时间。某次现场问题通过日志快速定位是手机厂商的ROM限制导致。
对于想要开发类似项目的开发者,我的建议是:
- 从第一天就开始构建模拟测试环境
- 实现协议解析的可视化工具
- 在真实设备上尽早测试
这个模块最终实现了:
- 99.5%的通信成功率
- 200ms以内的指令响应
- 全Android版本兼容
- 完善的错误处理机制
