本次补充协议核心缺失能力(BLE 分包 / 合包、CRC16 协议标准实现、心跳指令、异常上报解析)、稳定性能力(断连重连、帧合法性校验、日志封装)、完整解析能力(全错误码、配网失败原因、设备状态),并完善原有代码的容错性,让代码完全对齐协议文档要求,可直接落地联调。
所有代码基于Kotlin实现,适配 Android 4.3+(BLE 最低版本),兼容 Android 12/13/14 权限规则,核心类解耦,可直接模块化集成。
前置说明
- 协议核心对齐:严格遵循分包标识规则(首字节高 4 位总分包数、低 4 位当前序号)、合包 5 秒超时、CRC16 初始值 0xFFFF / 多项式 0x8005 / 结果取反、心跳 10 秒一次等协议要求;
- 权限完全适配:区分 Android 12+(BLUETOOTH_SCAN/CONNECT)和低版本(位置 + 蓝牙权限);
- 稳定性设计:断连自动重连(最多 3 次)、帧合法性校验(帧头 / 帧尾 / 版本)、合包超时丢弃、数据发送失败重试(最多 3 次);
- 全指令覆盖:包含握手、心跳、获取 WiFi 列表、配网请求、配网状态查询、核心状态查询等协议所有核心指令,及异常上报、全错误码、配网失败原因解析。
一、基础配置(无新增,补充细节)
1. AndroidManifest.xml 权限 & 配置
xml
<!-- 基础蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<!-- Android 12+ 蓝牙扫描/连接权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Android 12- 扫描BLE需要位置权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />
<!-- 网络权限(配网后云端通信) -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!-- 声明BLE支持,非强制(可选) -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<!-- 应用进程常驻(可选,提升蓝牙稳定性) -->
<application
...
android:process=":ble">
...
</application>
2. build.gradle(Module)
无需额外依赖,仅使用 Android 原生 API,确保编译版本≥26:
gradle
android {
compileSdk 34
defaultConfig {
minSdk 18 // 对应Android 4.3,BLE最低支持版本
targetSdk 34
...
}
...
}
dependencies {
// 协程(处理异步/定时任务,必加)
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
// 日志封装(可选,可替换为自己的日志库)
implementation 'com.orhanobut:logger:2.2.0'
}
二、核心通用工具类
1. 日志工具类(LogUtil.kt)
统一日志管理,方便联调排查,可替换为项目自有日志库:
kotlin
import com.orhanobut.logger.Logger
object LogUtil {
private const val TAG = "ChargingPile_BLE"
fun init(isDebug: Boolean) {
if (isDebug) {
Logger.init(TAG).setLogLevel(com.orhanobut.logger.LogLevel.FULL)
} else {
Logger.init(TAG).setLogLevel(com.orhanobut.logger.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(TAG, e, msg)
fun json(json: String) = Logger.json(json)
}
2. CRC16 校验工具类(协议标准实现,修正原有查表法)
严格对齐协议 CRC16 规则:初始值0xFFFF、多项式0x8005、逐字节处理、结果取反、小端序存储,摒弃查表法,确保与充电桩端计算结果一致。
kotlin
object CRC16Util {
// 协议指定:多项式0x8005,初始值0xFFFF
private const val POLYNOMIAL = 0x8005
private const val INIT_VALUE = 0xFFFF
/**
* 计算CRC16校验值(协议标准)
* @param data 待校验数据(协议指定:版本+帧类型+数据长度+指令码+数据域)
* @return CRC16结果(2字节,小端序存储)
*/
fun calculateCRC16(data: ByteArray): ByteArray {
var crc = INIT_VALUE
data.forEach { byte ->
crc = crc xor (byte.toInt() and 0xFF) shl 8
repeat(8) {
crc = if (crc and 0x8000 != 0) {
(crc shl 1) xor POLYNOMIAL
} else {
crc shl 1
}
}
}
// 结果取反 + 保留低16位
crc = crc.inv() and 0xFFFF
// 小端序存储(低字节在前,高字节在后)
return byteArrayOf(
(crc and 0xFF).toByte(),
(crc shr 8 and 0xFF).toByte()
)
}
/**
* 验证CRC16是否正确
* @param data 待校验数据
* @param crc16 待验证的CRC16字节(小端序)
* @return true=校验通过,false=校验失败
*/
fun verifyCRC16(data: ByteArray, crc16: ByteArray): Boolean {
if (crc16.size != 2) return false
val calculateCrc = calculateCRC16(data)
return calculateCrc[0] == crc16[0] && calculateCrc[1] == crc16[1]
}
}
3. BLE 分包 / 合包工具类(协议核心,新增)
严格遵循协议BLE 分包 / 合包规则:
- 分包:单帧超 20 字节时,首字节为高 4 位总分包数 + 低 4 位当前序号(从 0 开始);
- 合包:按序号拼接,首个分包接收后5 秒超时,超时丢弃所有数据;
- 单帧≤20 字节:直接发送,无需分包。
kotlin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.*
import kotlin.collections.HashMap
object BlePackageUtil {
// BLE单帧最大字节数(协议指定)
const val BLE_MAX_FRAME_LEN = 20
// 合包超时时间(协议指定:5秒)
private const val PACKAGE_MERGE_TIMEOUT = 5000L
// 合包缓存:key=设备地址,value=分包缓存对象
private val packageCache = HashMap<String, PackageCacheBean>()
/**
* BLE分包处理
* @param data 原始待发送数据(完整协议帧)
* @return 分包后的字节数组列表,单帧≤20字节时返回单元素列表
*/
fun splitPackage(data: ByteArray): List<ByteArray> {
val result = mutableListOf<ByteArray>()
if (data.size <= BLE_MAX_FRAME_LEN) {
// 无需分包,直接返回
result.add(data)
return result
}
// 计算总分包数:向上取整
val totalPackage = (data.size + BLE_MAX_FRAME_LEN - 1) / BLE_MAX_FRAME_LEN
if (totalPackage > 15) { // 高4位最大为15(0xF),限制总分包数≤15
LogUtil.e("分包数超过15,无法发送")
return result
}
// 逐包拆分
var currentIndex = 0
for (packageIndex in 0 until totalPackage) {
// 分包首字节:高4位=总分包数,低4位=当前序号
val head = (totalPackage shl 4) or packageIndex
// 计算当前包数据长度
val len = if (currentIndex + BLE_MAX_FRAME_LEN > data.size) {
data.size - currentIndex
} else {
BLE_MAX_FRAME_LEN
}
// 构建分包:首字节 + 数据段
val packageData = ByteArray(len + 1)
packageData[0] = head.toByte()
System.arraycopy(data, currentIndex, packageData, 1, len)
result.add(packageData)
currentIndex += len
}
LogUtil.d("分包完成:总分包数=$totalPackage,原始长度=${data.size}")
return result
}
/**
* BLE合包处理
* @param deviceAddress 蓝牙设备地址(唯一标识,防止多设备合包混乱)
* @param frame 接收到的单帧数据
* @return 合包后的完整数据(null=未合包完成/超时/分包异常)
*/
fun mergePackage(deviceAddress: String, frame: ByteArray): ByteArray? {
if (frame.isEmpty()) return null
// 单帧无分包标识(首字节不是分包头),直接返回
val head = frame[0].toInt() and 0xFF
val totalPackage = head shr 4 // 高4位:总分包数
val currentIndex = head and 0x0F // 低4位:当前序号
if (totalPackage == 0) return frame
// 校验分包合法性
if (currentIndex >= totalPackage || totalPackage > 15) {
LogUtil.e("分包异常:总分包数=$totalPackage,当前序号=$currentIndex")
clearCache(deviceAddress)
return null
}
// 初始化/获取合包缓存
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)
}
}
}
// 缓存当前分包
if (!cache.hasPackage(currentIndex)) {
cache.addPackage(currentIndex, frame.copyOfRange(1, frame.size))
LogUtil.d("缓存分包:设备=$deviceAddress,序号=$currentIndex,已缓存=${cache.getCachedCount()}/${totalPackage}")
}
// 所有分包缓存完成,拼接返回
return if (cache.isAllPackageCached()) {
val mergeData = cache.mergeAllPackage()
clearCache(deviceAddress)
LogUtil.d("合包完成:设备=$deviceAddress,完整长度=${mergeData.size}")
mergeData
} else {
null
}
}
/**
* 清除指定设备的合包缓存
*/
fun clearCache(deviceAddress: String) {
packageCache.remove(deviceAddress)
}
/**
* 清除所有合包缓存
*/
fun clearAllCache() {
packageCache.clear()
}
/**
* 合包缓存实体
* @param totalPackage 总分包数
*/
private data class PackageCacheBean(
val totalPackage: Int,
// 分包缓存:key=序号,value=分包数据
private val packageMap: SortedMap<Int, ByteArray> = TreeMap()
) {
// 是否包含指定序号分包
fun hasPackage(index: Int): Boolean = packageMap.containsKey(index)
// 添加分包
fun addPackage(index: Int, data: ByteArray) {
packageMap[index] = data
}
// 获取已缓存分包数
fun getCachedCount(): Int = packageMap.size
// 是否所有分包都已缓存
fun isAllPackageCached(): Boolean = packageMap.size == totalPackage
// 拼接所有分包为完整数据
fun mergeAllPackage(): ByteArray {
val totalLen = packageMap.values.sumOf { it.size }
val result = ByteArray(totalLen)
var currentIndex = 0
packageMap.values.forEach { data ->
System.arraycopy(data, 0, result, currentIndex, data.size)
currentIndex += data.size
}
return result
}
}
}
三、协议帧核心工具类(全功能扩展,修正原有逻辑)
新增帧合法性校验、心跳指令、核心状态查询、异常上报解析、全错误码 / 配网失败原因解析,完善指令构建,添加实体类封装解析结果,让指令收发 / 解析完全对齐协议。
1. 协议常量 & 实体类(ProtocolConstant.kt)
抽离协议所有常量,封装解析结果实体,解耦工具类:
kotlin
// 协议核心常量
object ProtocolConstant {
// 帧头/帧尾(十六进制)
const val FRAME_HEADER = 0xAA55
const val FRAME_TAIL = 0x55AA
// 协议版本
const val PROTOCOL_VERSION = 0x01
// 帧类型
const val FRAME_TYPE_REQUEST = 0x00 // 请求帧(App→桩)
const val FRAME_TYPE_RESPONSE = 0x01 // 响应帧(桩→App)
const val FRAME_TYPE_REPORT = 0x02 // 上报帧(桩主动→App)
// 指令码(协议定义,16进制)
const val CMD_HANDSHAKE = 0x0001 // 握手指令
const val CMD_HEARTBEAT = 0x0002 // 心跳指令
const val CMD_QUERY_CORE_STATUS = 0x0101// 核心状态查询
const val CMD_GET_WIFI_LIST = 0x0400 // 获取WiFi列表
const val CMD_WIFI_CONFIG = 0x0401 // WiFi配网请求
const val CMD_QUERY_CONFIG_STATUS = 0x0402// 配网状态查询
const val CMD_CONFIG_RESET = 0x0404 // 配网重置
const val CMD_REPORT_EXCEPTION = 0x0301 // 异常上报(桩主动)
// 设备状态(1字节)
const val DEVICE_STATUS_IDLE = 0x00 // 空闲
const val DEVICE_STATUS_CHARGING = 0x01 // 充电中
const val DEVICE_STATUS_FAULT = 0x02 // 故障
// 配网状态(1字节)
const val CONFIG_STATUS_DOING = 0x00 // 配网中
const val CONFIG_STATUS_SUCCESS = 0x01 // 配网成功
const val CONFIG_STATUS_FAILED = 0x02 // 配网失败
// 协议错误码(1字节,全量)
const val ERROR_SUCCESS = 0x00 // 成功
const val ERROR_CMD_NOT_SUPPORT = 0x01 // 指令不支持
const val ERROR_PARAM_INVALID = 0x02 // 参数格式错误
const val ERROR_DEVICE_BUSY = 0x03 // 设备忙
const val ERROR_CRC16_FAILED = 0x04 // CRC16校验失败
const val ERROR_VERIFY_CODE = 0x05 // 验证码错误
const val ERROR_PERMISSION_DENIED = 0x06// 权限不足
const val ERROR_HARDWARE_FAULT = 0x07 // 硬件故障
const val ERROR_CONFIG_DOING = 0x08 // 配网中,禁止重复操作
const val ERROR_CONFIG_TASK_ID_INVALID = 0x09// 配网任务ID无效
const val ERROR_WIFI_PARAM_TOO_LONG = 0x0A// WiFi参数超长
const val ERROR_WIFI_SCAN_FAILED = 0x0B // WiFi扫描失败
const val ERROR_WIFI_NONE = 0x0C // 无可用WiFi
// 配网失败原因码(1字节)
const val CONFIG_FAIL_SSID_NONE = 0x01 // WiFi SSID不存在
const val CONFIG_FAIL_PWD_ERROR = 0x02 // WiFi密码错误
const val CONFIG_FAIL_CONNECT_TIMEOUT = 0x03// WiFi连接超时
const val CONFIG_FAIL_CLOUD_CONNECT = 0x04// 云端接入失败
const val CONFIG_FAIL_WIFI_MODULE = 0x05// 硬件网络模块故障
// 异常类型(1字节,协议定义)
const val EXCEPTION_OVER_CURRENT = 0x01 // 过流
const val EXCEPTION_OVER_VOLTAGE = 0x02 // 过压
const val EXCEPTION_HIGH_TEMP = 0x03 // 高温
const val EXCEPTION_POWER_OFF = 0x04 // 断电
// WiFi加密方式(1字节)
const val WIFI_ENCRYPT_NONE = 0x00 // 未加密
const val WIFI_ENCRYPT_WPA2 = 0x01 // WPA2
const val WIFI_ENCRYPT_WPA3 = 0x02 // WPA3
const val WIFI_ENCRYPT_WEP = 0x03 // WEP
}
// WiFi信息实体(桩侧扫描结果)
data class WifiInfo(
val ssid: String, // WiFi名称
val rssi: Int, // 信号强度(dBm,负数)
val encryptType: Int // 加密方式(ProtocolConstant.WIFI_ENCRYPT_*)
) {
// 加密方式描述
fun getEncryptDesc(): String {
return when (encryptType) {
ProtocolConstant.WIFI_ENCRYPT_NONE -> "未加密"
ProtocolConstant.WIFI_ENCRYPT_WPA2 -> "WPA2"
ProtocolConstant.WIFI_ENCRYPT_WPA3 -> "WPA3"
ProtocolConstant.WIFI_ENCRYPT_WEP -> "WEP"
else -> "未知"
}
}
}
// 设备核心状态实体
data class DeviceCoreStatus(
val deviceStatus: Int, // 设备状态(ProtocolConstant.DEVICE_STATUS_*)
val remainPower: Int, // 剩余电量(%)
val chargePower: Int, // 充电功率(W)
val temp: Int // 温度(℃)
) {
// 设备状态描述
fun getDeviceStatusDesc(): String {
return when (deviceStatus) {
ProtocolConstant.DEVICE_STATUS_IDLE -> "空闲"
ProtocolConstant.DEVICE_STATUS_CHARGING -> "充电中"
ProtocolConstant.DEVICE_STATUS_FAULT -> "故障"
else -> "未知"
}
}
}
// 握手响应实体
data class HandshakeResponse(
val deviceId: String, // 设备ID(8字节)
val protocolVersion: Int,// 协议版本
val deviceStatus: Int // 设备状态
)
// 配网状态响应实体
data class ConfigStatusResponse(
val configStatus: Int, // 配网状态(ProtocolConstant.CONFIG_STATUS_*)
val failReason: Int? = null // 配网失败原因(仅失败时非空,ProtocolConstant.CONFIG_FAIL_*)
)
// 异常上报实体
data class ExceptionReported(
val exceptionType: Int, // 异常类型(ProtocolConstant.EXCEPTION_*)
val exceptionTime: Long, // 异常时间(Unix时间戳,秒)
val exceptionParam: Int // 异常参数(如过流值/过压值)
) {
// 异常类型描述
fun getExceptionDesc(): String {
return when (exceptionType) {
ProtocolConstant.EXCEPTION_OVER_CURRENT -> "过流"
ProtocolConstant.EXCEPTION_OVER_VOLTAGE -> "过压"
ProtocolConstant.EXCEPTION_HIGH_TEMP -> "高温"
ProtocolConstant.EXCEPTION_POWER_OFF -> "断电"
else -> "未知异常"
}
}
}
// 配网请求响应实体
data class ConfigRequestResponse(
val taskId: Short // 配网任务ID(2字节,小端序)
)
// 心跳响应实体
data class HeartbeatResponse(
val onlineStatus: Int, // 在线状态(0x01=在线,0x00=离线)
val signalStrength: Int // 信号强度(0-100)
)
2. 协议帧构建 / 解析工具类(ProtocolFrameUtil.kt,全功能版)
包含全指令构建、帧合法性校验、全响应 / 上报解析、Base64 编码、小端序处理,所有逻辑严格对齐协议,添加完整容错性。
kotlin
import android.util.Base64
import java.nio.ByteBuffer
import java.nio.ByteOrder
import ProtocolConstant.*
object ProtocolFrameUtil {
init {
// 初始化日志
LogUtil.init(true)
}
// Base64编码(无换行,适配蓝牙传输)
private fun base64Encode(str: String): ByteArray {
return Base64.encode(str.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
}
// 小端序转换:Int→2字节数组
private fun intTo2BytesLittleEndian(num: Int): ByteArray {
return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(num.toShort()).array()
}
// 小端序转换:Short→2字节数组
private fun shortTo2BytesLittleEndian(num: Short): ByteArray {
return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(num).array()
}
// 小端序转换:2字节数组→Int
private fun bytes2ToIntLittleEndian(bytes: ByteArray): Int {
if (bytes.size != 2) return 0
return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).short.toInt() and 0xFFFF
}
// 小端序转换:2字节数组→Short
private fun bytes2ToShortLittleEndian(bytes: ByteArray): Short {
if (bytes.size != 2) return -1
return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).short
}
// 4字节数组→Long(Unix时间戳)
private fun bytes4ToLong(bytes: ByteArray): Long {
if (bytes.size != 4) return 0
return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).int.toLong()
}
/**
* 帧合法性基础校验(协议必做)
* @param frame 接收到的完整帧数据
* @return true=校验通过,false=校验失败(帧头/帧尾/版本错误)
*/
fun checkFrameValid(frame: ByteArray): Boolean {
if (frame.size < 10) { // 最小帧长度:帧头2+版本1+帧类型1+数据长度2+指令码2+CRC162+帧尾2=12,容错设为10
LogUtil.e("帧长度过短:${frame.size}")
return false
}
// 校验帧头(前2字节)
val header = ByteBuffer.wrap(frame, 0, 2).short.toInt() and 0xFFFF
if (header != FRAME_HEADER) {
LogUtil.e("帧头错误:0x${Integer.toHexString(header)},期望0x${Integer.toHexString(FRAME_HEADER)}")
return false
}
// 校验协议版本(第3字节)
val version = frame[2].toInt() and 0xFF
if (version != PROTOCOL_VERSION) {
LogUtil.e("协议版本不匹配:0x${Integer.toHexString(version)},期望0x${Integer.toHexString(PROTOCOL_VERSION)}")
return false
}
// 校验帧尾(最后2字节)
val tail = ByteBuffer.wrap(frame, frame.size - 2, 2).short.toInt() and 0xFFFF
if (tail != FRAME_TAIL) {
LogUtil.e("帧尾错误:0x${Integer.toHexString(tail)},期望0x${Integer.toHexString(FRAME_TAIL)}")
return false
}
return true
}
/**
* 提取待校验数据并验证CRC16(协议必做)
* @param frame 完整帧数据
* @return Pair(待校验数据, 是否校验通过)
*/
fun extractCheckDataAndVerifyCRC16(frame: ByteArray): Pair<ByteArray?, Boolean> {
if (!checkFrameValid(frame)) return Pair(null, false)
// 待校验数据范围:版本(1) + 帧类型(1) + 数据长度(2) + 指令码(2) + 数据域(N)
// 数据长度位置:第4-5字节(小端序),表示「指令码2+数据域N」的总长度
val dataLen = bytes2ToIntLittleEndian(frame.copyOfRange(3, 5))
val checkDataLen = 1 + 1 + 2 + 2 + dataLen // 版本+帧类型+数据长度+指令码+数据域
val checkData = frame.copyOfRange(2, 2 + checkDataLen)
// 提取CRC16(待校验数据后2字节)
val crc16 = frame.copyOfRange(2 + checkDataLen, 2 + checkDataLen + 2)
// 验证CRC16
val isCrcValid = CRC16Util.verifyCRC16(checkData, crc16)
if (!isCrcValid) LogUtil.e("CRC16校验失败")
return Pair(checkData, isCrcValid)
}
/**
* 通用帧构建方法(所有指令的基础构建)
* @param frameType 帧类型(FRAME_TYPE_*)
* @param cmdCode 指令码(CMD_*)
* @param dataField 数据域字节数组(无则传空)
* @return 完整协议帧字节数组(可直接分包发送)
*/
fun buildFrame(frameType: Int, cmdCode: Int, dataField: ByteArray = byteArrayOf()): ByteArray {
val header = ByteBuffer.allocate(2).putShort(FRAME_HEADER.toShort()).array() // 帧头
val version = byteArrayOf(PROTOCOL_VERSION.toByte()) // 协议版本
val frameTypeByte = byteArrayOf(frameType.toByte()) // 帧类型
val dataLen = 2 + dataField.size // 数据长度:指令码2+数据域N
val dataLenBytes = intTo2BytesLittleEndian(dataLen) // 数据长度(小端序)
val cmdCodeBytes = intTo2BytesLittleEndian(cmdCode) // 指令码(小端序)
// 待校验数据
val checkData = ByteBuffer.allocate(version.size + frameTypeByte.size + dataLenBytes.size + cmdCodeBytes.size + dataField.size)
.put(version)
.put(frameTypeByte)
.put(dataLenBytes)
.put(cmdCodeBytes)
.put(dataField)
.array()
val crc16 = CRC16Util.calculateCRC16(checkData) // CRC16校验
val tail = ByteBuffer.allocate(2).putShort(FRAME_TAIL.toShort()).array() // 帧尾
// 拼接完整帧
return ByteBuffer.allocate(header.size + checkData.size + crc16.size + tail.size)
.put(header)
.put(checkData)
.put(crc16)
.put(tail)
.array()
}
// ---------------------- 指令构建(App→桩)----------------------
/**
* 构建握手指令帧
* @param appVersion App版本(如V1.0.0)
*/
fun buildHandshakeFrame(appVersion: String): ByteArray {
val dataField = appVersion.toByteArray(Charsets.UTF_8)
return buildFrame(FRAME_TYPE_REQUEST, CMD_HANDSHAKE, dataField)
}
/**
* 构建心跳指令帧(无数据域)
*/
fun buildHeartbeatFrame(): ByteArray {
return buildFrame(FRAME_TYPE_REQUEST, CMD_HEARTBEAT)
}
/**
* 构建核心状态查询帧(无数据域)
*/
fun buildQueryCoreStatusFrame(): ByteArray {
return buildFrame(FRAME_TYPE_REQUEST, CMD_QUERY_CORE_STATUS)
}
/**
* 构建获取WiFi列表帧(无数据域)
*/
fun buildGetWifiListFrame(): ByteArray {
return buildFrame(FRAME_TYPE_REQUEST, CMD_GET_WIFI_LIST)
}
/**
* 构建WiFi配网请求帧
* @param ssid 目标WiFi SSID(从桩侧列表选择)
* @param password WiFi密码(未加密则传空)
* @param timeout 配网超时时间(10~60秒)
*/
fun buildWifiConfigFrame(ssid: String, password: String, timeout: Int): ByteArray {
val ssidBase64 = base64Encode(ssid)
val pwdBase64 = base64Encode(password)
val timeoutByte = byteArrayOf(timeout.coerceIn(10, 60).toByte())
val dataField = ByteBuffer.allocate(ssidBase64.size + pwdBase64.size + timeoutByte.size)
.put(ssidBase64)
.put(pwdBase64)
.put(timeoutByte)
.array()
return buildFrame(FRAME_TYPE_REQUEST, CMD_WIFI_CONFIG, dataField)
}
/**
* 构建配网状态查询帧
* @param taskId 配网任务ID(从配网请求响应中获取)
*/
fun buildQueryConfigStatusFrame(taskId: Short): ByteArray {
val dataField = shortTo2BytesLittleEndian(taskId)
return buildFrame(FRAME_TYPE_REQUEST, CMD_QUERY_CONFIG_STATUS, dataField)
}
/**
* 构建配网重置帧(无数据域)
*/
fun buildConfigResetFrame(): ByteArray {
return buildFrame(FRAME_TYPE_REQUEST, CMD_CONFIG_RESET)
}
// ---------------------- 响应/上报解析(桩→App)----------------------
/**
* 解析错误码(所有响应帧的第一个数据域字节)
* @param frame 完整响应帧
* @return 错误码(ERROR_*),解析失败返回-1
*/
fun parseErrorCode(frame: ByteArray): Int {
val (checkData, isCrcValid) = extractCheckDataAndVerifyCRC16(frame)
if (checkData == null || !isCrcValid) return -1
// 错误码位置:checkData[6](版本1+帧类型1+数据长度2+指令码2 =6,后续为数据域,第一个字节是错误码)
if (checkData.size <7) return -1
return checkData[6].toInt() and 0xFF
}
/**
* 解析握手响应帧
* @param frame 完整响应帧
* @return HandshakeResponse,解析失败返回null
*/
fun parseHandshakeResponse(frame: ByteArray): HandshakeResponse? {
val errorCode = parseErrorCode(frame)
if (errorCode != ERROR_SUCCESS) {
LogUtil.e("握手失败,错误码:0x${Integer.toHexString(errorCode)}")
return null
}
val (checkData, _) = extractCheckDataAndVerifyCRC16(frame) ?: return null
// 数据域:设备ID(8) + 协议版本(1) + 设备状态(1)
if (checkData.size < 6+8+1+1) return null
val deviceIdBytes = checkData.copyOfRange(7, 7+8)
val deviceId = String(deviceIdBytes, Charsets.UTF_8)
val protocolVersion = checkData[15].toInt() and 0xFF
val deviceStatus = checkData[16].toInt() and 0xFF
return HandshakeResponse(deviceId, protocolVersion, deviceStatus)
}
/**
* 解析心跳响应帧
* @param frame 完整响应帧
* @return HeartbeatResponse,解析失败返回null
*/
fun parseHeartbeatResponse(frame: ByteArray): HeartbeatResponse? {
if (parseErrorCode(frame) != ERROR_SUCCESS) return null
val (checkData, _) = extractCheckDataAndVerifyCRC16(frame) ?: return null
// 数据域:在线状态(1) + 信号强度(1)
if (checkData.size <6+2) return null
val onlineStatus = checkData[7].toInt() and 0xFF
val signalStrength = checkData[8].toInt() and 0xFF
return HeartbeatResponse(onlineStatus, signalStrength)
}
/**
* 解析核心状态查询响应帧
* @param frame 完整响应帧
* @return DeviceCoreStatus,解析失败返回null
*/
fun parseDeviceCoreStatus(frame: ByteArray): DeviceCoreStatus? {
if (parseErrorCode(frame) != ERROR_SUCCESS) return null
val (checkData, _) = extractCheckDataAndVerifyCRC16(frame) ?: return null
// 数据域:设备状态(1) + 剩余电量(2) + 充电功率(2) + 温度(1)
if (checkData.size <6+1+2+2+1) return null
val deviceStatus = checkData[7].toInt() and 0xFF
val remainPower = bytes2ToIntLittleEndian(checkData.copyOfRange(8,10))
val chargePower = bytes2ToIntLittleEndian(checkData.copyOfRange(10,12))
val temp = checkData[12].toInt() and 0xFF
return DeviceCoreStatus(deviceStatus, remainPower, chargePower, temp)
}
/**
* 解析WiFi列表响应帧
* @param frame 完整响应帧
* @return List<WifiInfo>,解析失败返回空列表
*/
fun parseWifiList(frame: ByteArray): List<WifiInfo> {
val wifiList = mutableListOf<WifiInfo>()
if (parseErrorCode(frame) != ERROR_SUCCESS) return wifiList
val (checkData, _) = extractCheckDataAndVerifyCRC16(frame) ?: return wifiList
// 数据域:WiFi数量(1) + N个WiFi项(SSID长度(1)+SSID(M)+信号强度(1)+加密方式(1))
if (checkData.size <7) return wifiList
val wifiCount = checkData[7].toInt() and 0xFF
if (wifiCount ==0) {
LogUtil.i("桩侧无可用WiFi")
return wifiList
}
var currentIndex =8
repeat(wifiCount) {
if (currentIndex +3 > checkData.size) return@repeat
// SSID长度
val ssidLen = checkData[currentIndex].toInt() and 0xFF
currentIndex++
if (currentIndex + ssidLen > checkData.size) return@repeat
// SSID
val ssidBytes = checkData.copyOfRange(currentIndex, currentIndex+ssidLen)
val ssid = String(ssidBytes, Charsets.UTF_8)
currentIndex += ssidLen
// 信号强度(字节为绝对值,实际为负数)
val rssiAbs = checkData[currentIndex].toInt() and 0xFF
val rssi = -rssiAbs
currentIndex++
// 加密方式
val encryptType = checkData[currentIndex].toInt() and 0xFF
currentIndex++
wifiList.add(WifiInfo(ssid, rssi, encryptType))
}
LogUtil.i("解析到WiFi列表:共${wifiList.size}个")
return wifiList
}
/**
* 解析配网请求响应帧
* @param frame 完整响应帧
* @return ConfigRequestResponse,解析失败返回null
*/
fun parseConfigRequestResponse(frame: ByteArray): ConfigRequestResponse? {
if (parseErrorCode(frame) != ERROR_SUCCESS) return null
val (checkData, _) = extractCheckDataAndVerifyCRC16(frame) ?: return null
// 数据域:配网任务ID(2)
if (checkData.size <6+2) return null
val taskId = bytes2ToShortLittleEndian(checkData.copyOfRange(7,9))
LogUtil.i("配网任务ID:$taskId")
return ConfigRequestResponse(taskId)
}
/**
* 解析配网状态查询响应帧
* @param frame 完整响应帧
* @return ConfigStatusResponse,解析失败返回null
*/
fun parseConfigStatusResponse(frame: ByteArray): ConfigStatusResponse? {
if (parseErrorCode(frame) != ERROR_SUCCESS) return null
val (checkData, _) = extractCheckDataAndVerifyCRC16(frame) ?: return null
// 数据域:配网状态(1) + [失败原因(1)](仅失败时存在)
if (checkData.size <7) return null
val configStatus = checkData[7].toInt() and 0xFF
val failReason = if (configStatus == CONFIG_STATUS_FAILED && checkData.size >=8) {
checkData[8].toInt() and 0xFF
} else {
null
}
return ConfigStatusResponse(configStatus, failReason)
}
/**
* 解析异常上报帧(桩主动发送,上报帧无错误码)
* @param frame 完整上报帧
* @return ExceptionReported,解析失败返回null
*/
fun parseExceptionReported(frame: ByteArray): ExceptionReported? {
val (checkData, isCrcValid) = extractCheckDataAndVerifyCRC16(frame)
if (checkData == null || !isCrcValid) return null
// 上报帧无错误码,数据域:异常类型(1) + 异常时间(4) + 异常参数(2)
if (checkData.size <6+1+4+2) return null
val exceptionType = checkData[6].toInt() and 0xFF
val exceptionTime = bytes4ToLong(checkData.copyOfRange(7,11))
val exceptionParam = bytes2ToIntLittleEndian(checkData.copyOfRange(11,13))
LogUtil.e("收到桩异常上报:${ExceptionReported(exceptionType, exceptionTime, exceptionParam).getExceptionDesc()}")
return ExceptionReported(exceptionType, exceptionTime, exceptionParam)
}
}
四、BLE 蓝牙管理核心类(BLEManager.kt,稳定版)
集成分包发送、合包接收、断连重连(3 次)、数据发送重试(3 次)、心跳定时发送(10 秒)、蓝牙状态监听,封装所有 BLE 核心操作,对外提供简洁的调用接口,解耦上层业务。
kotlin
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.os.Build
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.UUID
import java.util.concurrent.atomic.AtomicInteger
class BLEManager private constructor(private val context: Context) {
// 蓝牙适配器
private val bluetoothAdapter: BluetoothAdapter? by lazy {
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
}
// 协议指定的UUID(与充电桩端严格一致,不可修改)
private val SERVICE_UUID = UUID.fromString("0000FF00-0000-1000-8000-00805F9B34FB")
private val CHAR_UUID = UUID.fromString("0000FF01-0000-1000-8000-00805F9B34FB")
// BLE核心对象
private var bluetoothGatt: BluetoothGatt? = null
private var communicationChar: BluetoothGattCharacteristic? = null
private var connectedDevice: BluetoothDevice? = null // 已连接设备
// 重试/重连计数
private val reconnectCount = AtomicInteger(0)
private val maxReconnectCount = 3 // 最大重连次数
// 定时任务:心跳、重连
private var heartbeatJob: Job? = null
private var reconnectJob: Job? = null
// 数据发送重试
private val sendDataRetryCount = AtomicInteger(0)
private val maxSendRetryCount = 3 // 最大发送重试次数
// 对外回调接口(所有BLE事件通知)
interface BLECallback {
fun onDeviceScanned(device: BluetoothDevice) // 扫描到充电桩设备
fun onConnectionStateChanged(isConnected: Boolean, device: BluetoothDevice?) // 连接状态变化
fun onDataReceived(frame: ByteArray) // 接收到完整协议帧(已合包/校验)
fun onDataSendSuccess() // 数据发送成功
fun onDataSendFailed() // 数据发送失败(重试后仍失败)
}
private var bleCallback: BLECallback? = null
fun setBLECallback(callback: BLECallback) {
this.bleCallback = callback
}
// 初始化日志
init {
LogUtil.init(true)
}
// ---------------------- 蓝牙基础状态检查 ----------------------
/**
* 检查蓝牙是否可用(已开启+支持BLE)
*/
fun isBluetoothAvailable(): Boolean {
return bluetoothAdapter != null && bluetoothAdapter!!.isEnabled &&
context.packageManager.hasSystemFeature(android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE)
}
/**
* 打开蓝牙(跳转到系统蓝牙设置)
*/
fun openBluetooth() {
if (bluetoothAdapter?.isEnabled == false) {
context.startActivity(android.content.Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE).apply {
addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
})
}
}
// ---------------------- BLE扫描 ----------------------
@SuppressLint("MissingPermission")
fun startScan() {
if (!isBluetoothAvailable()) {
LogUtil.w("蓝牙不可用,无法扫描")
return
}
val scanCallback = object : android.bluetooth.le.ScanCallback() {
override fun onScanResult(callbackType: Int, result: android.bluetooth.le.ScanResult) {
super.onScanResult(callbackType, result)
val device = result.device
// 过滤充电桩设备(协议指定名称前缀:NewEnergy_)
if (device.name?.startsWith("NewEnergy_") == true) {
LogUtil.d("扫描到充电桩设备:${device.name} | ${device.address}")
bleCallback?.onDeviceScanned(device)
}
}
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
LogUtil.e("扫描失败,错误码:$errorCode")
}
}
// 开始扫描(30秒后自动停止,避免耗电)
bluetoothAdapter?.bluetoothLeScanner?.startScan(scanCallback)
CoroutineScope(Dispatchers.IO).launch {
delay(30000L)
stopScan(scanCallback)
LogUtil.i("扫描超时,自动停止")
}
}
@SuppressLint("MissingPermission")
fun stopScan(scanCallback: android.bluetooth.le.ScanCallback) {
bluetoothAdapter?.bluetoothLeScanner?.stopScan(scanCallback)
}
// ---------------------- BLE连接/断连 ----------------------
@SuppressLint("MissingPermission")
fun connect(device: BluetoothDevice) {
// 断开原有连接
disconnect()
connectedDevice = device
reconnectCount.set(0)
LogUtil.i("开始连接设备:${device.name} | ${device.address}")
bluetoothGatt = device.connectGatt(context, false, gattCallback)
}
@SuppressLint("MissingPermission")
fun disconnect() {
// 取消定时任务
heartbeatJob?.cancel()
reconnectJob?.cancel()
// 清除合包缓存
BlePackageUtil.clearCache(connectedDevice?.address ?: "")
// 断开GATT连接
bluetoothGatt?.disconnect()
bluetoothGatt?.close()
bluetoothGatt = null
communicationChar = null
// 重置状态
connectedDevice = null
reconnectCount.set(0)
sendDataRetryCount.set(0)
LogUtil.i("BLE连接已断开")
bleCallback?.onConnectionStateChanged(false, null)
}
/**
* 自动重连(最多3次)
*/
@SuppressLint("MissingPermission")
private fun autoReconnect() {
if (reconnectCount.get() >= maxReconnectCount) {
LogUtil.e("重连次数达上限($maxReconnectCount),停止重连")
disconnect()
return
}
val device = connectedDevice ?: return
reconnectCount.incrementAndGet()
LogUtil.i("开始第${reconnectCount.get()}次重连:${device.address}")
reconnectJob = CoroutineScope(Dispatchers.IO).launch {
delay(2000L) // 延迟2秒重连,避免频繁连接
bluetoothGatt = device.connectGatt(context, false, gattCallback)
}
}
// ---------------------- 数据发送(含分包/重试)----------------------
@SuppressLint("MissingPermission")
fun sendData(data: ByteArray) {
if (bluetoothGatt == null || communicationChar == null || connectedDevice == null) {
LogUtil.e("BLE未连接,无法发送数据")
bleCallback?.onDataSendFailed()
return
}
// 分包处理
val splitPackages = BlePackageUtil.splitPackage(data)
if (splitPackages.isEmpty()) {
LogUtil.e("分包失败,无法发送")
bleCallback?.onDataSendFailed()
return
}
// 逐包发送(BLE为异步,此处简化为直接发送,如需严格顺序可加队列)
splitPackages.forEach { packageData ->
communicationChar?.value = packageData
val isSend = bluetoothGatt?.writeCharacteristic(communicationChar) ?: false
if (!isSend) {
LogUtil.e("单包发送失败,开始重试:${sendDataRetryCount.get()+1}")
if (sendDataRetryCount.incrementAndGet() >= maxSendRetryCount) {
sendDataRetryCount.set(0)
bleCallback?.onDataSendFailed()
} else {
CoroutineScope(Dispatchers.IO).launch {
delay(500L)
sendData(data)
}
}
}
}
sendDataRetryCount.set(0)
}
// ---------------------- 心跳定时发送(协议指定10秒一次)----------------------
private fun startHeartbeat() {
// 取消原有心跳
heartbeatJob?.cancel()
// 启动新心跳:10秒一次,首次立即发送
heartbeatJob = CoroutineScope(Dispatchers.IO).launch {
sendData(ProtocolFrameUtil.buildHeartbeatFrame())
LogUtil.d("首次发送心跳指令")
while (true) {
delay(10000L)
sendData(ProtocolFrameUtil.buildHeartbeatFrame())
LogUtil.d("定时发送心跳指令")
}
}
}
// ---------------------- GATT核心回调 ----------------------
private val gattCallback = object : BluetoothGattCallback() {
// 连接状态变化
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)
val device = gatt.device
if (newState == BluetoothProfile.STATE_CONNECTED) {
// 连接成功,重置重连计数,发现服务
reconnectCount.set(0)
LogUtil.i("设备连接成功:${device.address}")
CoroutineScope(Dispatchers.IO).launch {
gatt.discoverServices()
}
bleCallback?.onConnectionStateChanged(true, device)
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
// 连接断开,触发自动重连
LogUtil.w("设备连接断开:${device.address},状态码:$status")
bleCallback?.onConnectionStateChanged(false, device)
autoReconnect()
}
}
// 服务发现完成
@SuppressLint("MissingPermission")
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
super.onServicesDiscovered(gatt, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
// 获取协议指定的服务和特征值
val service: BluetoothGattService? = gatt.getService(SERVICE_UUID)
communicationChar = service?.getCharacteristic(CHAR_UUID)
if (communicationChar == null) {
LogUtil.e("未找到协议指定的通信特征值,连接失败")
disconnect()
return
}
// 启用特征值通知(接收充电桩主动上报数据:异常、配网结果)
setCharacteristicNotification(gatt, communicationChar!!, true)
LogUtil.i("服务发现成功,已启用特征值通知")
// 启动心跳(协议要求10秒一次)
startHeartbeat()
} else {
LogUtil.e("服务发现失败,状态码:$status")
disconnect()
}
}
// 数据写入结果(发送数据到充电桩后的回调)
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
super.onCharacteristicWrite(gatt, characteristic, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
LogUtil.d("数据发送成功")
bleCallback?.onDataSendSuccess()
} else {
LogUtil.e("数据写入失败,状态码:$status")
bleCallback?.onDataSendFailed()
}
}
// 接收到充电桩数据(主动上报/响应)
@SuppressLint("MissingPermission")
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
super.onCharacteristicChanged(gatt, characteristic)
val receiveData = characteristic.value ?: return
val deviceAddress = gatt.device.address
LogUtil.d("接收到设备数据:$deviceAddress,长度:${receiveData.size}")
// 合包处理(协议指定BLE分包规则)
val completeFrame = BlePackageUtil.mergePackage(deviceAddress, receiveData)
completeFrame ?: return
// 帧合法性基础校验(帧头、帧尾、版本)
if (!ProtocolFrameUtil.checkFrameValid(completeFrame)) {
LogUtil.e("接收到非法帧,跳过解析")
return
}
// 回调上层完整合法帧
bleCallback?.onDataReceived(completeFrame)
}
}
// ---------------------- 启用特征值通知(固定逻辑)----------------------
@SuppressLint("MissingPermission")
private fun setCharacteristicNotification(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
enabled: Boolean
) {
gatt.setCharacteristicNotification(characteristic, enabled)
// 启用CCCD描述符(BLE通知必备,固定UUID)
val cccdUuid = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
val descriptor = characteristic.getDescriptor(cccdUuid) ?: return
descriptor.value = if (enabled) {
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
} else {
BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
}
gatt.writeDescriptor(descriptor)
}
// ---------------------- 单例实现(双重校验锁,避免内存泄漏)----------------------
companion object {
@Volatile
private var INSTANCE: BLEManager? = null
fun getInstance(context: Context): BLEManager {
return INSTANCE ?: synchronized(this) {
val instance = BLEManager(context.applicationContext)
INSTANCE = instance
instance
}
}
// 销毁单例(应用退出时调用)
fun destroy() {
INSTANCE?.disconnect()
INSTANCE = null
}
}
}
五、完整业务调用示例(ChargingPileBleActivity.kt)
基于Activity实现从 BLE 扫描→连接→握手→获取 WiFi 列表→配网→状态查询→异常处理的全流程业务,适配 Android 12 + 权限规则,包含完整的生命周期管理和用户交互提示,可直接作为页面基类使用。
kotlin
import android.Manifest
import android.bluetooth.BluetoothDevice
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import com.orhanobut.logger.Logger
/**
* 充电桩蓝牙配网+通信主页面
* 实现全流程:扫描→连接→握手→获取WiFi列表→配网→状态查询→异常解析
*/
class ChargingPileBleActivity : AppCompatActivity() {
// BLE管理器
private val bleManager by lazy { BLEManager.getInstance(this) }
// 业务状态
private var connectedDevice: BluetoothDevice? = null // 已连接的充电桩设备
private var configTaskId: Short = -1 // 配网任务ID
private var isConfigSuccess = false // 是否配网成功
// 权限请求码
private val PERMISSION_REQUEST_CODE = 1001
// 配网超时查询次数(2秒一次,共30次=60秒)
private val MAX_CONFIG_QUERY_COUNT = 30
private var currentConfigQueryCount = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_ble_config) // 替换为自己的布局
initBleCallback()
// 初始化日志
LogUtil.init(BuildConfig.DEBUG)
// 检查并申请权限
checkAndRequestPermissions()
}
// ---------------------- 初始化BLE回调(核心业务逻辑)----------------------
private fun initBleCallback() {
bleManager.setBLECallback(object : BLEManager.BLECallback {
override fun onDeviceScanned(device: BluetoothDevice) {
// 扫描到充电桩,直接连接(也可展示列表让用户选择)
if (ActivityCompat.checkSelfPermission(
this@ChargingPileBleActivity,
Manifest.permission.BLUETOOTH_CONNECT
) != PackageManager.PERMISSION_GRANTED
) {
return
}
if (connectedDevice == null) {
connectedDevice = device
bleManager.connect(device)
}
}
override fun onConnectionStateChanged(isConnected: Boolean, device: BluetoothDevice?) {
runOnUiThread {
if (isConnected) {
Toast.makeText(this@ChargingPileBleActivity, "设备连接成功:${device?.name}", Toast.LENGTH_SHORT).show()
// 连接成功后立即发送握手指令(协议要求:仅握手成功可执行其他指令)
sendHandshakeCmd()
} else {
Toast.makeText(this@ChargingPileBleActivity, "设备连接断开", Toast.LENGTH_SHORT).show()
resetBusinessState()
}
}
}
override fun onDataReceived(frame: ByteArray) {
// 接收到充电桩数据,解析并处理
parseReceiveFrame(frame)
}
override fun onDataSendSuccess() {
runOnUiThread {
// Toast.makeText(this@ChargingPileBleActivity, "数据发送成功", Toast.LENGTH_SHORT).show()
}
}
override fun onDataSendFailed() {
runOnUiThread {
Toast.makeText(this@ChargingPileBleActivity, "数据发送失败", Toast.LENGTH_SHORT).show()
}
}
})
}
// ---------------------- 权限处理(适配Android 12+)----------------------
private fun checkAndRequestPermissions() {
val needRequestPermissions = mutableListOf<String>()
// 按Android版本区分权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+:仅需蓝牙扫描/连接权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
needRequestPermissions.add(Manifest.permission.BLUETOOTH_SCAN)
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
needRequestPermissions.add(Manifest.permission.BLUETOOTH_CONNECT)
}
} else {
// Android 12-:蓝牙权限+位置权限(BLE扫描需要)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) != PackageManager.PERMISSION_GRANTED) {
needRequestPermissions.add(Manifest.permission.BLUETOOTH)
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
needRequestPermissions.add(Manifest.permission.BLUETOOTH_ADMIN)
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
needRequestPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION)
}
}
// 申请权限
if (needRequestPermissions.isNotEmpty()) {
ActivityCompat.requestPermissions(this, needRequestPermissions.toTypedArray(), PERMISSION_REQUEST_CODE)
} else {
// 权限已获取,检查蓝牙并开始扫描
startBleScan()
}
}
// 权限申请结果回调
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSION_REQUEST_CODE) {
val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
if (allGranted) {
startBleScan()
} else {
Toast.makeText(this, "请授予所有蓝牙权限,否则无法使用功能", Toast.LENGTH_LONG).show()
finish()
}
}
}
// ---------------------- BLE扫描启动 ----------------------
private fun startBleScan() {
if (bleManager.isBluetoothAvailable()) {
bleManager.startScan()
Toast.makeText(this, "开始扫描充电桩设备...", Toast.LENGTH_SHORT).show()
} else {
// 蓝牙未开启,跳转到系统设置
bleManager.openBluetooth()
Toast.makeText(this, "请开启蓝牙后重试", Toast.LENGTH_SHORT).show()
}
}
// ---------------------- 指令发送(上层业务调用)----------------------
/** 发送握手指令 */
private fun sendHandshakeCmd() {
val frame = ProtocolFrameUtil.buildHandshakeFrame("V1.0.0") // 替换为实际App版本
bleManager.sendData(frame)
LogUtil.i("已发送握手指令")
}
/** 发送获取WiFi列表指令 */
private fun sendGetWifiListCmd() {
val frame = ProtocolFrameUtil.buildGetWifiListFrame()
bleManager.sendData(frame)
LogUtil.i("已发送获取WiFi列表指令")
}
/** 发送配网请求指令 */
private fun sendWifiConfigCmd(ssid: String, pwd: String, timeout: Int = 30) {
val frame = ProtocolFrameUtil.buildWifiConfigFrame(ssid, pwd, timeout)
bleManager.sendData(frame)
LogUtil.i("已发送配网请求:SSID=$ssid,超时=$timeout秒")
}
/** 发送配网状态查询指令 */
private fun sendQueryConfigStatusCmd() {
if (configTaskId == -1.toShort()) {
LogUtil.w("配网任务ID无效,无法查询状态")
return
}
val frame = ProtocolFrameUtil.buildQueryConfigStatusFrame(configTaskId)
bleManager.sendData(frame)
LogUtil.i("已发送配网状态查询:任务ID=$configTaskId")
}
/** 发送配网重置指令 */
private fun sendConfigResetCmd() {
val frame = ProtocolFrameUtil.buildConfigResetFrame()
bleManager.sendData(frame)
LogUtil.i("已发送配网重置指令")
}
/** 发送核心状态查询指令 */
private fun sendQueryCoreStatusCmd() {
val frame = ProtocolFrameUtil.buildQueryCoreStatusFrame()
bleManager.sendData(frame)
LogUtil.i("已发送设备核心状态查询指令")
}
// ---------------------- 接收帧解析(核心协议解析)----------------------
private fun parseReceiveFrame(frame: ByteArray) {
// 1. 先解析错误码(响应帧通用)
val errorCode = ProtocolFrameUtil.parseErrorCode(frame)
if (errorCode != ProtocolConstant.ERROR_SUCCESS && errorCode != -1) {
runOnUiThread {
Toast.makeText(this, "操作失败,错误码:0x${Integer.toHexString(errorCode)}", Toast.LENGTH_SHORT).show()
}
LogUtil.e("指令执行失败,错误码:0x${Integer.toHexString(errorCode)}")
// 配网中重复操作,直接返回
if (errorCode == ProtocolConstant.ERROR_CONFIG_DOING) return
// 配网任务ID无效,重置任务ID
if (errorCode == ProtocolConstant.ERROR_CONFIG_TASK_ID_INVALID) configTaskId = -1
return
}
// 2. 解析具体指令响应/上报
// 握手响应
val handshakeResp = ProtocolFrameUtil.parseHandshakeResponse(frame)
if (handshakeResp != null) {
LogUtil.i("握手成功:设备ID=${handshakeResp.deviceId},设备状态=${handshakeResp.getDeviceStatusDesc()}")
runOnUiThread {
Toast.makeText(this, "握手成功,设备ID:${handshakeResp.deviceId}", Toast.LENGTH_SHORT).show()
}
// 握手成功后,立即获取WiFi列表(配网前置步骤)
sendGetWifiListCmd()
return
}
// WiFi列表响应
val wifiList = ProtocolFrameUtil.parseWifiList(frame)
if (wifiList.isNotEmpty()) {
val wifiDesc = wifiList.joinToString { "${it.ssid}(${it.rssi}dBm,${it.getEncryptDesc()})" }
LogUtil.i("获取WiFi列表成功:$wifiDesc")
runOnUiThread {
Toast.makeText(this, "获取到${wifiList.size}个WiFi,开始配网", Toast.LENGTH_SHORT).show()
}
// 选择第一个WiFi配网(实际业务可让用户选择,此处简化)
val targetWifi = wifiList[0]
val wifiPwd = "12345678" // 替换为用户输入的密码,未加密则传空
sendWifiConfigCmd(targetWifi.ssid, wifiPwd)
return
}
// 配网请求响应(获取任务ID)
val configReqResp = ProtocolFrameUtil.parseConfigRequestResponse(frame)
if (configReqResp != null) {
configTaskId = configReqResp.taskId
runOnUiThread {
Toast.makeText(this, "配网请求接收成功,任务ID:$configTaskId", Toast.LENGTH_SHORT).show()
}
// 开始轮询查询配网状态(2秒一次)
startQueryConfigStatusLoop()
return
}
// 配网状态响应
val configStatusResp = ProtocolFrameUtil.parseConfigStatusResponse(frame)
if (configStatusResp != null) {
currentConfigQueryCount++
val statusDesc = when (configStatusResp.configStatus) {
ProtocolConstant.CONFIG_STATUS_DOING -> "配网中..."
ProtocolConstant.CONFIG_STATUS_SUCCESS -> {
isConfigSuccess = true
"配网成功!"
}
ProtocolConstant.CONFIG_STATUS_FAILED -> {
val failReason = configStatusResp.failReason ?: -1
val failDesc = when (failReason) {
ProtocolConstant.CONFIG_FAIL_SSID_NONE -> "SSID不存在"
ProtocolConstant.CONFIG_FAIL_PWD_ERROR -> "密码错误"
ProtocolConstant.CONFIG_FAIL_CONNECT_TIMEOUT -> "WiFi连接超时"
else -> "未知错误(0x${Integer.toHexString(failReason)})"
}
"配网失败:$failDesc"
}
else -> "未知状态"
}
LogUtil.i("配网状态查询:$statusDesc,查询次数:$currentConfigQueryCount/$MAX_CONFIG_QUERY_COUNT")
runOnUiThread {
Toast.makeText(this, statusDesc, Toast.LENGTH_SHORT).show()
}
// 配网成功/失败/超时,停止轮询
if (isConfigSuccess || configStatusResp.configStatus == ProtocolConstant.CONFIG_STATUS_FAILED || currentConfigQueryCount >= MAX_CONFIG_QUERY_COUNT) {
currentConfigQueryCount = 0
if (currentConfigQueryCount >= MAX_CONFIG_QUERY_COUNT) {
runOnUiThread {
Toast.makeText(this, "配网超时", Toast.LENGTH_SHORT).show()
}
}
// 配网成功后,查询设备核心状态
if (isConfigSuccess) {
sendQueryCoreStatusCmd()
}
return
}
}
// 设备核心状态响应
val coreStatus = ProtocolFrameUtil.parseDeviceCoreStatus(frame)
if (coreStatus != null) {
val statusDesc = "设备状态:${coreStatus.getDeviceStatusDesc()},剩余电量:${coreStatus.remainPower}%,充电功率:${coreStatus.chargePower}W,温度:${coreStatus.temp}℃"
LogUtil.i("设备核心状态:$statusDesc")
runOnUiThread {
Toast.makeText(this, statusDesc, Toast.LENGTH_LONG).show()
}
return
}
// 异常上报(充电桩主动发送,无错误码)
val exceptionReported = ProtocolFrameUtil.parseExceptionReported(frame)
if (exceptionReported != null) {
val exceptionDesc = "设备异常:${exceptionReported.getExceptionDesc()},参数:${exceptionReported.exceptionParam},时间:${exceptionReported.exceptionTime}"
LogUtil.e(exceptionDesc)
runOnUiThread {
Toast.makeText(this, exceptionDesc, Toast.LENGTH_LONG).show()
}
return
}
// 心跳响应
val heartbeatResp = ProtocolFrameUtil.parseHeartbeatResponse(frame)
if (heartbeatResp != null) {
LogUtil.d("心跳响应:在线=${heartbeatResp.onlineStatus == 0x01},信号强度=${heartbeatResp.signalStrength}%")
}
}
// ---------------------- 轮询查询配网状态(2秒一次)----------------------
private fun startQueryConfigStatusLoop() {
CoroutineScope(Dispatchers.Main).launch {
while (currentConfigQueryCount < MAX_CONFIG_QUERY_COUNT && !isConfigSuccess) {
delay(2000L)
sendQueryConfigStatusCmd()
}
}
}
// ---------------------- 业务状态重置 ----------------------
private fun resetBusinessState() {
connectedDevice = null
configTaskId = -1
isConfigSuccess = false
currentConfigQueryCount = 0
}
// ---------------------- 生命周期管理(关键,避免内存泄漏)----------------------
override fun onPause() {
super.onPause()
// 暂停时断开BLE连接
bleManager.disconnect()
}
override fun onDestroy() {
super.onDestroy()
// 销毁时重置业务状态,销毁BLE单例
resetBusinessState()
BLEManager.destroy()
}
}
六、布局文件示例(activity_ble_config.xml)
简单布局,包含配网状态展示、重置配网按钮,可根据实际业务扩展:
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/tv_config_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="配网状态:未开始"
android:textSize="18sp"/>
<Button
android:id="@+id/btn_reset_config"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="重置配网"/>
</LinearLayout>
七、联调核心注意事项(必看)
代码开发完成后,与充电桩硬件联调是关键,以下是协议对齐和 Android 端的核心注意点,避免联调踩坑:
1. 协议层严格对齐
- UUID 一致:充电桩端的服务 UUID / 特征值 UUID必须与代码中
SERVICE_UUID/CHAR_UUID完全一致(协议指定:0000FF00-0000-1000-8000-00805F9B34FB/0000FF01-0000-1000-8000-00805F9B34FB); - CRC16 校验:充电桩端的 CRC16 实现必须与
CRC16Util.kt完全一致(初始值 0xFFFF、多项式 0x8005、结果取反、小端序),建议双方用同一组测试数据验证; - 小端序处理:多字节数据(指令码、长度、任务 ID)双方必须均为小端序存储,这是联调最常见的问题;
- 分包 / 合包规则:充电桩端的分包首字节格式(高 4 位总分包数、低 4 位当前序号)、合包 5 秒超时规则必须与
BlePackageUtil.kt一致。
2. Android 端适配
- 设备名称过滤:充电桩的 BLE 广播名称必须以NewEnergy_ 为前缀,否则代码会过滤掉;
- BLE 版本:充电桩需支持蓝牙 4.0+ BLE,Android 端测试机最低为 Android 4.3(API 18);
- 权限申请:Android 12 + 必须动态申请
BLUETOOTH_SCAN/BLUETOOTH_CONNECT,且BLUETOOTH_SCAN需添加neverForLocation标识; - 后台扫描:Android 10 + 后台扫描 BLE 需要
ACCESS_BACKGROUND_LOCATION权限,如需后台配网需额外申请; - 数据发送顺序:BLE 为异步发送,若需严格指令顺序(如握手→获取 WiFi 列表),需在数据发送成功回调中执行下一个指令,避免并发发送导致帧混乱。
3. 硬件端联调测试
- 单帧测试:先测试≤20 字节的短帧(如心跳、获取 WiFi 列表),再测试超长帧的分包 / 合包(如 10 个 WiFi 列表项);
- 异常测试:模拟 CRC16 错误、指令码不存在、配网参数错误,验证 Android 端能正确解析错误码;
- 断连测试:模拟配网过程中蓝牙断连,验证 Android 端的自动重连(3 次) 功能;
- 超时测试:模拟 WiFi 无信号,验证充电桩端在指定超时时间后返回配网失败,Android 端停止轮询。
4. 日志调试
- 开启代码中的日志(
LogUtil.init(true)),通过 Logcat 过滤ChargingPile_BLE标签,查看帧的发送 / 接收、解析结果; - 充电桩端开启串口日志,打印接收到的字节数组、CRC16 校验结果、指令执行状态,与 Android 端日志对比定位问题。
八、功能扩展建议
基于当前代码,可根据实际业务需求快速扩展以下功能:
- 充电控制指令:添加启动充电、停止充电、暂停充电的指令构建 / 解析(参考协议中
0x0201/0x0202/0x0203指令码); - 故障详情查询:实现协议中
0x0102指令的收发,解析充电桩故障码和描述; - 用户输入 WiFi 密码:在布局中添加 EditText,让用户手动输入 WiFi 密码,替代代码中的固定密码;
- WiFi 列表展示:用 RecyclerView 展示充电桩扫描到的 WiFi 列表,支持用户选择目标 WiFi;
- 持久化存储:将配网成功的 WiFi 参数、设备 ID 持久化到 SP/MMKV,避免重复配网;
- 蓝牙多设备管理:扩展 BLEManager 支持多充电桩设备连接,通过设备地址区分;
- 进度条展示:在配网过程中添加进度条,替代 Toast 提示,提升用户体验;
- 异常重连配网:配网失败后,支持一键重新获取 WiFi 列表并配网。
总结
本次编写的 Android BLE 模块代码完全对齐充电桩蓝牙通信协议,涵盖协议帧构建 / 解析、CRC16 校验、BLE 分包 / 合包、心跳定时、断连重连、全指令收发等核心功能,同时适配 Android 4.3 + 所有版本的权限和 API 规则,代码解耦性强、容错性高,可直接模块化集成到项目中。
联调的核心是协议层的严格对齐(尤其是 CRC16、小端序、分包规则),建议先通过简单指令(心跳、握手)完成基础通信,再逐步测试配网、状态查询等复杂功能,通过两端日志对比快速定位问题。
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/qq_35460159/article/details/158844594



