2 changed files with 442 additions and 233 deletions
@ -0,0 +1,297 @@ |
|||
package com.yunqiinnovation.ble_service |
|||
|
|||
import android.Manifest |
|||
import android.bluetooth.BluetoothGatt |
|||
import android.bluetooth.BluetoothGattCharacteristic |
|||
import android.os.Handler |
|||
import android.os.Looper |
|||
import android.util.Log |
|||
import androidx.annotation.RequiresPermission |
|||
import java.util.* |
|||
|
|||
/** |
|||
* BLE指令发送器类:负责处理蓝牙设备的指令发送和队列管理 |
|||
* 功能包括: |
|||
* 1. 指令队列管理 |
|||
* 2. 指令封装和CRC校验 |
|||
* 3. 指令发送和重试机制 |
|||
* 4. 响应超时处理 |
|||
*/ |
|||
class BleCommandSender { |
|||
|
|||
companion object { |
|||
private const val TAG = "BleCommandSender" |
|||
private const val REPLY_TIMEOUT_MS = 1000L // 响应超时时间1秒 |
|||
} |
|||
|
|||
// ====================================================================================================== |
|||
// 成员变量 |
|||
// ====================================================================================================== |
|||
|
|||
/** 指令队列 */ |
|||
private val commandQueue: Queue<ByteArray> = LinkedList() |
|||
|
|||
/** 是否正在等待回复 */ |
|||
var isWaitingReply = true |
|||
private set |
|||
|
|||
/** 回复的指令类型 */ |
|||
var replyCommandType = 0xff |
|||
private set |
|||
|
|||
/** 响应超时处理器 */ |
|||
private val replyTimeoutHandler = Handler(Looper.getMainLooper()) |
|||
|
|||
/** 响应超时回调 */ |
|||
private val replyTimeoutRunnable = Runnable { |
|||
if (!isWaitingReply) { |
|||
Log.w(TAG, "命令响应超时,自动重置 isWaitingReply 为 true") |
|||
isWaitingReply = true |
|||
processNextCommand() |
|||
} |
|||
} |
|||
|
|||
/** 指令发送状态回调接口 */ |
|||
interface CommandSenderCallback { |
|||
/** |
|||
* 检查连接状态 |
|||
* @return 是否已连接 |
|||
*/ |
|||
fun isConnected(): Boolean |
|||
|
|||
/** |
|||
* 获取写入特征 |
|||
* @return 写入特征对象 |
|||
*/ |
|||
fun getWriteCharacteristic(): BluetoothGattCharacteristic? |
|||
|
|||
/** |
|||
* 获取GATT连接 |
|||
* @return GATT连接对象 |
|||
*/ |
|||
fun getBluetoothGatt(): BluetoothGatt? |
|||
} |
|||
|
|||
private var callback: CommandSenderCallback? = null |
|||
|
|||
// ====================================================================================================== |
|||
// 公共方法 |
|||
// ====================================================================================================== |
|||
|
|||
/** |
|||
* 设置回调接口 |
|||
* @param callback 回调接口实现 |
|||
*/ |
|||
fun setCallback(callback: CommandSenderCallback) { |
|||
this.callback = callback |
|||
} |
|||
|
|||
/** |
|||
* 发送命令到设备 - 通用方法 |
|||
* @param cmdType 命令类型 |
|||
* @param cmdData 命令数据 |
|||
* @return 是否成功发送 |
|||
*/ |
|||
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) |
|||
fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean { |
|||
val callback = this.callback ?: run { |
|||
Log.e(TAG, "发送命令失败: 回调接口未设置") |
|||
return false |
|||
} |
|||
|
|||
if (!callback.isConnected()) { |
|||
Log.e(TAG, "发送命令失败: 设备未连接") |
|||
return false |
|||
} |
|||
|
|||
try { |
|||
// 创建命令包 |
|||
val cmdPacket = createCommandPacket(cmdType, cmdData) |
|||
|
|||
// 记录命令详情 |
|||
logCommandDetails(cmdType, cmdData, cmdPacket) |
|||
|
|||
// 获取写入特征 |
|||
val writeChar = callback.getWriteCharacteristic() ?: run { |
|||
Log.e(TAG, "发送命令失败: 写入特征未找到") |
|||
return false |
|||
} |
|||
|
|||
// 设置数据并发送 |
|||
writeChar.value = cmdPacket |
|||
|
|||
return if (isWaitingReply) { |
|||
// 直接发送 |
|||
val gatt = callback.getBluetoothGatt() |
|||
val isSuccess = gatt?.writeCharacteristic(writeChar) == true |
|||
if (isSuccess) { |
|||
isWaitingReply = false |
|||
startReplyTimeout() |
|||
true |
|||
} else { |
|||
Log.e(TAG, "发送命令失败: 写入特征失败") |
|||
false |
|||
} |
|||
} else { |
|||
// 加入队列等待发送 |
|||
commandQueue.offer(cmdPacket) |
|||
Log.d(TAG, "命令已加入队列,当前队列长度: ${commandQueue.size}") |
|||
true |
|||
} |
|||
|
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "发送命令异常: ${e.message}", e) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理命令响应,重置等待状态并处理下一个命令 |
|||
* @param responseCommandType 响应的命令类型 |
|||
*/ |
|||
fun onCommandResponse(responseCommandType: Int) { |
|||
isWaitingReply = true |
|||
replyCommandType = responseCommandType |
|||
replyTimeoutHandler.removeCallbacks(replyTimeoutRunnable) |
|||
processNextCommand() |
|||
} |
|||
|
|||
/** |
|||
* 清空命令队列 |
|||
*/ |
|||
fun clearCommandQueue() { |
|||
commandQueue.clear() |
|||
Log.d(TAG, "命令队列已清空") |
|||
} |
|||
|
|||
/** |
|||
* 获取当前队列长度 |
|||
* @return 队列中待发送的命令数量 |
|||
*/ |
|||
fun getQueueSize(): Int = commandQueue.size |
|||
|
|||
/** |
|||
* 重置发送状态 |
|||
*/ |
|||
fun reset() { |
|||
isWaitingReply = true |
|||
replyCommandType = 0xff |
|||
replyTimeoutHandler.removeCallbacks(replyTimeoutRunnable) |
|||
clearCommandQueue() |
|||
Log.d(TAG, "指令发送器已重置") |
|||
} |
|||
|
|||
// ====================================================================================================== |
|||
// 私有方法 |
|||
// ====================================================================================================== |
|||
|
|||
/** |
|||
* 创建命令包 |
|||
* @param cmdType 命令类型 |
|||
* @param cmdData 命令数据 |
|||
* @return 完整的命令包(包含帧头、命令、长度、数据、CRC) |
|||
*/ |
|||
private fun createCommandPacket(cmdType: Byte, cmdData: ByteArray): ByteArray { |
|||
val cmdLength = cmdData.size |
|||
val cmdPacket = ByteArray(4 + cmdLength) // 帧头 + 命令 + 长度 + 数据 + CRC |
|||
|
|||
// 帧头 |
|||
cmdPacket[0] = BleConst.APP_REQUEST_HEADER |
|||
// 命令类型 |
|||
cmdPacket[1] = cmdType |
|||
// 命令长度 |
|||
cmdPacket[2] = cmdLength.toByte() |
|||
|
|||
// 填充命令数据(如果有) |
|||
if (cmdLength > 0) { |
|||
System.arraycopy(cmdData, 0, cmdPacket, 3, cmdLength) |
|||
} |
|||
|
|||
// 计算CRC - 对帧头到数据的所有字节进行校验 (不包括CRC自身) |
|||
val crcPayload = cmdPacket.sliceArray(0 until cmdPacket.size - 1) |
|||
val crc = calculateCrc8Maxim(crcPayload, crcPayload.size) |
|||
|
|||
// 设置CRC |
|||
cmdPacket[3 + cmdLength] = crc |
|||
|
|||
return cmdPacket |
|||
} |
|||
|
|||
/** |
|||
* 记录命令详情 |
|||
* @param cmdType 命令类型 |
|||
* @param cmdData 命令数据 |
|||
* @param cmdPacket 完整命令包 |
|||
*/ |
|||
private fun logCommandDetails(cmdType: Byte, cmdData: ByteArray, cmdPacket: ByteArray) { |
|||
val crc = cmdPacket[cmdPacket.size - 1] |
|||
Log.d( |
|||
TAG, |
|||
"发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=${cmdData.size}, " + |
|||
"数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " + |
|||
"CRC=0x${(crc.toInt() and 0xFF).toString(16)}" |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* 处理队列中的下一个命令 |
|||
*/ |
|||
private fun processNextCommand() { |
|||
if (commandQueue.isEmpty()) { |
|||
return |
|||
} |
|||
|
|||
val callback = this.callback ?: return |
|||
val nextCmd = commandQueue.poll() ?: return |
|||
|
|||
val writeChar = callback.getWriteCharacteristic() ?: run { |
|||
Log.e(TAG, "处理下一个命令失败: 写入特征未找到") |
|||
return |
|||
} |
|||
|
|||
val gatt = callback.getBluetoothGatt() ?: run { |
|||
Log.e(TAG, "处理下一个命令失败: GATT连接未找到") |
|||
return |
|||
} |
|||
|
|||
writeChar.value = nextCmd |
|||
val isSuccess = gatt.writeCharacteristic(writeChar) |
|||
if (isSuccess) { |
|||
isWaitingReply = false |
|||
startReplyTimeout() |
|||
Log.d(TAG, "发送队列中的下一个命令,剩余队列长度: ${commandQueue.size}") |
|||
} else { |
|||
Log.e(TAG, "发送队列中的命令失败") |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 启动响应超时计时器 |
|||
*/ |
|||
private fun startReplyTimeout() { |
|||
replyTimeoutHandler.postDelayed(replyTimeoutRunnable, REPLY_TIMEOUT_MS) |
|||
} |
|||
|
|||
/** |
|||
* 计算CRC8/MAXIM校验值 |
|||
* @param data 要校验的数据 |
|||
* @param length 数据长度 |
|||
* @return CRC8校验值 |
|||
*/ |
|||
private fun calculateCrc8Maxim(data: ByteArray, length: Int): Byte { |
|||
var crc = 0 |
|||
val polynomial = 0x31 |
|||
|
|||
for (i in 0 until length) { |
|||
crc = crc xor (data[i].toInt() and 0xFF) |
|||
for (j in 0 until 8) { |
|||
if (crc and 0x80 != 0) { |
|||
crc = ((crc shl 1) xor polynomial) and 0xFF |
|||
} else { |
|||
crc = (crc shl 1) and 0xFF |
|||
} |
|||
} |
|||
} |
|||
return crc.toByte() |
|||
} |
|||
} |
|||
Loading…
Reference in new issue