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