44 changed files with 3371 additions and 1624 deletions
Binary file not shown.
Binary file not shown.
@ -0,0 +1,700 @@ |
|||
package com.yunqiinnovation.ble_service |
|||
|
|||
import android.Manifest |
|||
import android.annotation.SuppressLint |
|||
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. 响应超时处理 |
|||
* 5. 设备响应和通知处理 |
|||
*/ |
|||
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? |
|||
|
|||
/** |
|||
* 通知设备信息接收 |
|||
* @param infoType 信息类型 |
|||
* @param infoData 信息数据 |
|||
*/ |
|||
fun notifyDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) |
|||
|
|||
/** |
|||
* 通知唤醒信号接收 |
|||
*/ |
|||
fun notifyWakeupSignalReceived() |
|||
|
|||
/** |
|||
* 获取录音文件对象 |
|||
* @return 录音文件对象 |
|||
*/ |
|||
fun getRecordFile(): RecordingFile? |
|||
|
|||
/** |
|||
* 获取重新压缩录音文件对象 |
|||
* @return 重新压缩录音文件对象 |
|||
*/ |
|||
fun getRecordFile1(): RecordingFile? |
|||
} |
|||
|
|||
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() |
|||
} |
|||
|
|||
/** |
|||
* 处理设备响应数据 - 从BleService移动过来 |
|||
* @param data 响应数据 |
|||
*/ |
|||
@SuppressLint("StaticFieldLeak") |
|||
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) |
|||
fun processDeviceResponse(data: ByteArray) { |
|||
Log.d( |
|||
TAG, |
|||
"收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" |
|||
) |
|||
if (data.size < 3) { |
|||
Log.e(TAG, "设备响应数据格式错误:数据长度过短") |
|||
return |
|||
} |
|||
|
|||
val header = data[0] // 0xBB |
|||
val cmdType = data[1].toInt() and 0xFF |
|||
|
|||
// 判断命令失败响应(长度为0) |
|||
if (data.size == 4 && data[2].toInt() and 0xFF == 0) { |
|||
Log.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败") |
|||
// 创建失败结果Map |
|||
val resultMap = mapOf( |
|||
"success" to false, |
|||
"command" to cmdType, |
|||
"rawData" to byteArrayOf() |
|||
) |
|||
callback?.notifyDeviceInfoReceived(cmdType, resultMap) |
|||
// 通知指令发送器处理响应 |
|||
onCommandResponse(cmdType) |
|||
return |
|||
} |
|||
|
|||
// 正常响应处理 |
|||
val length = data[2].toInt() and 0xFF |
|||
|
|||
// 检查数据长度 |
|||
if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC |
|||
Log.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") |
|||
return |
|||
} |
|||
|
|||
// 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节 |
|||
val payload = data.sliceArray(0 until data.size - 1) |
|||
val calculatedCrc = calculateCrc8Maxim(payload, payload.size) |
|||
val receivedCrc = data[data.size - 1].toInt() and 0xFF |
|||
|
|||
if (calculatedCrc.toInt() and 0xFF != receivedCrc) { |
|||
Log.e( |
|||
TAG, |
|||
"设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" |
|||
) |
|||
return |
|||
} |
|||
|
|||
// 提取响应数据 |
|||
val responseData = if (length > 0) ByteArray(length) else ByteArray(0) |
|||
if (length > 0) { |
|||
System.arraycopy(data, 3, responseData, 0, length) |
|||
} |
|||
|
|||
Log.d( |
|||
TAG, |
|||
"收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${ |
|||
responseData.joinToString(", ") { |
|||
"0x${ |
|||
(it.toInt() and 0xFF).toString(16) |
|||
}" |
|||
} |
|||
}" |
|||
) |
|||
// 通知指令发送器处理响应 |
|||
onCommandResponse(cmdType) |
|||
// 根据命令类型处理不同的响应 |
|||
when (cmdType) { |
|||
BleConst.CMD_GET_VERSION -> { |
|||
val resultMap = mutableMapOf<String, Any>( |
|||
"success" to true, |
|||
"command" to "versionInfo", |
|||
"rawData" to responseData |
|||
) |
|||
|
|||
if (responseData.isNotEmpty()) { |
|||
// 转换为字符串前,确认是否是有效的ASCII字符 |
|||
val version = |
|||
responseData |
|||
.map { it.toInt() and 0xFF } // 转为无符号整数 (0-255) |
|||
.dropWhile { it == 0xFF } // 仅丢弃开头的连续 255 |
|||
.joinToString(".") { it.toString() } |
|||
resultMap["version"] = version |
|||
Log.i(TAG, "接收到目标版本号: $version") |
|||
} |
|||
|
|||
callback?.notifyDeviceInfoReceived(cmdType, resultMap) |
|||
} |
|||
|
|||
BleConst.CMD_GET_PRODUCT_ID -> { |
|||
val resultMap = mutableMapOf<String, Any>( |
|||
"success" to true, |
|||
"command" to "productId", |
|||
"rawData" to responseData |
|||
) |
|||
|
|||
if (responseData.isNotEmpty()) { |
|||
// 转换为字符串前,确认是否是有效的ASCII字符 |
|||
val productId = |
|||
responseData.joinToString(".") { (it.toInt() and 0xFF).toString() } |
|||
resultMap["productId"] = productId |
|||
Log.i(TAG, "接收到产品ID: $productId") |
|||
} |
|||
|
|||
callback?.notifyDeviceInfoReceived(cmdType, resultMap) |
|||
} |
|||
|
|||
BleConst.CMD_GET_COLOR_ID -> { |
|||
val resultMap = mutableMapOf<String, Any>( |
|||
"success" to true, |
|||
"command" to "colorInfo", |
|||
"rawData" to responseData |
|||
) |
|||
|
|||
if (responseData.isNotEmpty()) { |
|||
val colorId = (responseData[0].toInt() and 0xFF).toString() |
|||
resultMap["colorId"] = colorId |
|||
Log.i(TAG, "接收到耳机颜色ID: $colorId") |
|||
} |
|||
|
|||
callback?.notifyDeviceInfoReceived(cmdType, resultMap) |
|||
} |
|||
|
|||
BleConst.CMD_GET_BATTERY_INFO -> { |
|||
val resultMap = mutableMapOf<String, Any>( |
|||
"success" to true, |
|||
"command" to "batteryInfo", |
|||
"rawData" to responseData |
|||
) |
|||
|
|||
if (responseData.isNotEmpty() && responseData.size >= 3) { |
|||
// 处理电池信息 - 左耳、右耳、充电仓 |
|||
val leftEarBattery = responseData[0].toInt() and 0x7F |
|||
val leftEarCharging = (responseData[0].toInt() and 0x80) != 0 |
|||
|
|||
val rightEarBattery = |
|||
if (responseData.size > 1) responseData[1].toInt() and 0x7F else 0 |
|||
val rightEarCharging = |
|||
if (responseData.size > 1) (responseData[1].toInt() and 0x80) != 0 else false |
|||
|
|||
val caseBattery = |
|||
if (responseData.size > 2) responseData[2].toInt() and 0x7F else 0 |
|||
val caseCharging = |
|||
if (responseData.size > 2) (responseData[2].toInt() and 0x80) != 0 else false |
|||
|
|||
// 添加到结果Map |
|||
resultMap["leftEarBattery"] = leftEarBattery |
|||
resultMap["leftEarCharging"] = leftEarCharging |
|||
resultMap["rightEarBattery"] = rightEarBattery |
|||
resultMap["rightEarCharging"] = rightEarCharging |
|||
resultMap["caseBattery"] = caseBattery |
|||
resultMap["caseCharging"] = caseCharging |
|||
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 |
|||
|
|||
Log.i( |
|||
TAG, "接收到电量信息: 左耳=${leftEarBattery}% " + |
|||
"(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + |
|||
"(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + |
|||
"(${if (caseCharging) "充电中" else "未充电"})" |
|||
) |
|||
} |
|||
|
|||
callback?.notifyDeviceInfoReceived(cmdType, resultMap) |
|||
} |
|||
|
|||
BleConst.CMD_CONTROL_CODEC -> { |
|||
val resultMap = mutableMapOf<String, Any>( |
|||
"success" to true, |
|||
"command" to cmdType, |
|||
"rawData" to responseData |
|||
) |
|||
|
|||
if (responseData.isNotEmpty() && responseData.size >= 2) { |
|||
val codecStatus = responseData[0].toInt() and 0xFF |
|||
val channelMode = responseData[1].toInt() and 0xFF |
|||
|
|||
// 解码状态描述 |
|||
val statusDesc = when (codecStatus) { |
|||
BleConst.CODEC_CONTROL_CLOSE -> "已关闭编解码" |
|||
BleConst.CODEC_CONTROL_DECODE_ON -> "已打开解码" |
|||
BleConst.CODEC_CONTROL_A2DP_PLAY -> "A2DP播放模式" |
|||
BleConst.CODEC_CONTROL_CALL_RECORD_PLAY -> "通话记录播放模式" |
|||
BleConst.CODEC_CONTROL_ENCODE_ON -> "已打开编码" |
|||
else -> "未知状态($codecStatus)" |
|||
} |
|||
|
|||
// 处理录音文件 |
|||
if (codecStatus == BleConst.CODEC_CONTROL_DECODE_ON || |
|||
codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY || |
|||
codecStatus == BleConst.CODEC_CONTROL_CALL_RECORD_PLAY || |
|||
codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON |
|||
) { |
|||
callback?.getRecordFile1()?.closeFile() |
|||
callback?.getRecordFile1()?.creatingFiles() |
|||
callback?.getRecordFile()?.closeFile() |
|||
callback?.getRecordFile()?.creatingFiles() |
|||
} else if (codecStatus == BleConst.CODEC_CONTROL_CLOSE) { |
|||
callback?.getRecordFile()?.closeFile() |
|||
callback?.getRecordFile1()?.closeFile() |
|||
} |
|||
|
|||
Log.d(TAG, "收到设备响应,channelMode: $channelMode") |
|||
|
|||
// 声道模式描述 |
|||
val channelDesc = when (channelMode) { |
|||
BleConst.AUDIO_CHANNEL_LEFT -> "左声道" |
|||
BleConst.AUDIO_CHANNEL_RIGHT -> "右声道" |
|||
BleConst.AUDIO_CHANNEL_STEREO -> "立体声" |
|||
else -> "未知声道($channelMode)" |
|||
} |
|||
|
|||
resultMap["codecStatus"] = codecStatus |
|||
resultMap["channelMode"] = channelMode |
|||
resultMap["statusDesc"] = statusDesc |
|||
resultMap["channelDesc"] = channelDesc |
|||
|
|||
Log.i(TAG, "编解码控制响应: $statusDesc, $channelDesc") |
|||
} else if (responseData.isNotEmpty() && responseData.size == 1) { |
|||
val codecStatus = responseData[0].toInt() and 0xFF |
|||
if (codecStatus == 0x00) { |
|||
Log.d(TAG, "收到设备响应:失败,codecStatus: $codecStatus") |
|||
val resultMap = mutableMapOf<String, Any>( |
|||
"success" to true, |
|||
"command" to "Error", |
|||
"rawData" to responseData |
|||
) |
|||
} |
|||
} |
|||
|
|||
callback?.notifyDeviceInfoReceived(cmdType, resultMap) |
|||
} |
|||
|
|||
else -> { |
|||
// 对于未知命令,返回原始数据 |
|||
val resultMap = mapOf( |
|||
"success" to true, |
|||
"command" to cmdType, |
|||
"rawData" to responseData, |
|||
"isUnknown" to true |
|||
) |
|||
Log.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}") |
|||
callback?.notifyDeviceInfoReceived(cmdType, resultMap) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理设备主动上报数据 - 帧头为0xCC |
|||
* 将可见性修改为internal,允许同包内的BleService类访问 |
|||
*/ |
|||
fun processDeviceNotification(data: ByteArray) { |
|||
Log.d( |
|||
TAG, |
|||
"收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" |
|||
) |
|||
if (data.size < 3) { |
|||
Log.e(TAG, "设备主动上报数据格式错误:数据长度过短") |
|||
return |
|||
} |
|||
|
|||
val header = data[0] // 0xCC |
|||
val infoType = data[1].toInt() and 0xFF |
|||
val length = data[2].toInt() and 0xFF |
|||
|
|||
// 检查数据长度 |
|||
if (data.size != length + 4) { // 帧头 + 类型 + 长度 + 数据 + CRC |
|||
Log.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") |
|||
return |
|||
} |
|||
|
|||
// 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节 |
|||
val payload = data.sliceArray(0 until data.size - 1) |
|||
val calculatedCrc = calculateCrc8Maxim(payload, payload.size) |
|||
val receivedCrc = data[data.size - 1].toInt() and 0xFF |
|||
|
|||
if (calculatedCrc.toInt() and 0xFF != receivedCrc) { |
|||
Log.e( |
|||
TAG, |
|||
"设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" |
|||
) |
|||
return |
|||
} |
|||
|
|||
// 提取上报数据 |
|||
val notifyData = if (length > 0) ByteArray(length) else ByteArray(0) |
|||
if (length > 0) { |
|||
System.arraycopy(data, 3, notifyData, 0, length) |
|||
} |
|||
|
|||
Log.d( |
|||
TAG, |
|||
"收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${ |
|||
notifyData.joinToString(", ") { |
|||
"0x${ |
|||
(it.toInt() and 0xFF).toString(16) |
|||
}" |
|||
} |
|||
}" |
|||
) |
|||
|
|||
// 根据上报类型处理 |
|||
when (infoType) { |
|||
BleConst.CMD_GET_BATTERY_INFO -> { // 电量信息 |
|||
val resultMap = mutableMapOf<String, Any>( |
|||
"success" to true, |
|||
"command" to infoType, |
|||
"rawData" to notifyData, |
|||
"isNotification" to true // 标记为主动上报 |
|||
) |
|||
|
|||
if (notifyData.isNotEmpty() && notifyData.size >= 3) { |
|||
// 处理电池信息 - 左耳、右耳、充电仓 |
|||
val leftEarBattery = notifyData[0].toInt() and 0x7F |
|||
val leftEarCharging = (notifyData[0].toInt() and 0x80) != 0 |
|||
|
|||
val rightEarBattery = |
|||
if (notifyData.size > 1) notifyData[1].toInt() and 0x7F else 0 |
|||
val rightEarCharging = |
|||
if (notifyData.size > 1) (notifyData[1].toInt() and 0x80) != 0 else false |
|||
|
|||
val caseBattery = if (notifyData.size > 2) notifyData[2].toInt() and 0x7F else 0 |
|||
val caseCharging = |
|||
if (notifyData.size > 2) (notifyData[2].toInt() and 0x80) != 0 else false |
|||
|
|||
|
|||
// 添加到结果Map |
|||
resultMap["leftEarBattery"] = leftEarBattery |
|||
resultMap["leftEarCharging"] = leftEarCharging |
|||
resultMap["rightEarBattery"] = rightEarBattery |
|||
resultMap["rightEarCharging"] = rightEarCharging |
|||
resultMap["caseBattery"] = caseBattery |
|||
resultMap["caseCharging"] = caseCharging |
|||
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 |
|||
|
|||
Log.i( |
|||
TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " + |
|||
"(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + |
|||
"(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + |
|||
"(${if (caseCharging) "充电中" else "未充电"})" |
|||
) |
|||
} |
|||
|
|||
callback?.notifyDeviceInfoReceived(infoType, resultMap) |
|||
} |
|||
|
|||
BleConst.CMD_WAKE_UP -> { |
|||
Log.i(TAG, "收到唤醒指令") |
|||
// 接收到唤醒信号,打开编码器 (设备侧) |
|||
|
|||
callback?.notifyWakeupSignalReceived() |
|||
|
|||
} |
|||
// 可以在这里添加其他主动上报类型的处理 |
|||
else -> { |
|||
// 对于未知上报类型,返回原始数据 |
|||
val resultMap = mapOf( |
|||
"success" to true, |
|||
"command" to infoType, |
|||
"rawData" to notifyData, |
|||
"isUnknown" to true, |
|||
"isNotification" to true // 标记为主动上报 |
|||
) |
|||
Log.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}") |
|||
callback?.notifyDeviceInfoReceived(infoType, resultMap) |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 清空命令队列 |
|||
*/ |
|||
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() |
|||
} |
|||
} |
|||
@ -1,72 +0,0 @@ |
|||
package com.yunqiinnovation.ble_service |
|||
|
|||
import android.companion.AssociationInfo |
|||
import android.companion.CompanionDeviceService |
|||
import android.content.Intent |
|||
import android.os.Build |
|||
import android.os.IBinder |
|||
import androidx.annotation.RequiresApi |
|||
import android.util.Log |
|||
|
|||
/** |
|||
* BleCompanionService |
|||
* 用于监听配对设备的出现和消失 |
|||
* 注:此服务仅在Android 11 (API 30)及以上版本可用 |
|||
*/ |
|||
@RequiresApi(Build.VERSION_CODES.R) |
|||
class BleCompanionService : CompanionDeviceService() { |
|||
private val TAG = "BleCompanionService" |
|||
|
|||
override fun onCreate() { |
|||
super.onCreate() |
|||
Log.i(TAG, "BleCompanionService已创建") |
|||
} |
|||
|
|||
override fun onDeviceAppeared(info: AssociationInfo) { |
|||
super.onDeviceAppeared(info) |
|||
Log.i(TAG, "检测到配对设备出现: ${info.displayName}, ID: ${info.id}") |
|||
|
|||
// 当配对设备出现时,启动前台服务 |
|||
try { |
|||
val serviceIntent = Intent().apply { |
|||
// 尝试使用BleMediaService,如果不存在则回退到BleService |
|||
val serviceClassName = try { |
|||
Class.forName("com.yunqiinnovation.agent_service.BleMediaService").name |
|||
} catch (e: ClassNotFoundException) { |
|||
BleService::class.java.name |
|||
} |
|||
|
|||
setClassName(this@BleCompanionService, serviceClassName) |
|||
action = "android.intent.action.DEVICE_APPEARED" |
|||
putExtra("assocId", info.id) |
|||
putExtra("displayName", info.displayName) |
|||
|
|||
// 如果设备信息中有MAC地址,也可以传递 |
|||
// 使用反射获取MAC地址,因为deviceMacAddress在不同API版本可能不同或不存在 |
|||
try { |
|||
val macAddressMethod = info.javaClass.getMethod("getDeviceMacAddress") |
|||
val mac = macAddressMethod.invoke(info) as? String |
|||
mac?.let { putExtra("mac", it) } |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e) |
|||
} |
|||
} |
|||
|
|||
// 启动前台服务 |
|||
startForegroundService(serviceIntent) |
|||
Log.i(TAG, "已启动服务") |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "启动服务失败: ${e.message}", e) |
|||
} |
|||
} |
|||
|
|||
override fun onDeviceDisappeared(info: AssociationInfo) { |
|||
super.onDeviceDisappeared(info) |
|||
Log.i(TAG, "配对设备消失: ${info.displayName}, ID: ${info.id}") |
|||
|
|||
// 可以选择停止服务或执行其他清理操作 |
|||
} |
|||
|
|||
// 注意: 不需要重写 onBindCompanionDeviceService 和 onUnbindCompanionDeviceService |
|||
// 这些方法在 Android 12+ 才添加,我们可以使用父类的默认实现 |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,506 @@ |
|||
package com.yunqiinnovation.ble_service |
|||
|
|||
import android.os.Handler |
|||
import android.os.Looper |
|||
import android.util.Log |
|||
import com.jieli.jl_audio_decode.opus.model.OpusOption |
|||
import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback |
|||
import com.jieli.jl_audio_decode.callback.OnEncodeStreamCallback |
|||
import com.jieli.jl_audio_decode.opus.OpusManager |
|||
import com.jieli.jl_audio_decode.exceptions.OpusException |
|||
import java.util.concurrent.LinkedBlockingQueue |
|||
import java.util.concurrent.atomic.AtomicBoolean |
|||
|
|||
/** |
|||
* Opus音频编解码管理器 |
|||
* 负责处理音频数据的Opus编码和解码功能 |
|||
* |
|||
* 功能包括: |
|||
* 1. Opus解码流管理 |
|||
* 2. Opus编码流管理 |
|||
* 3. 音频数据缓冲和处理 |
|||
* 4. 编解码状态管理 |
|||
*/ |
|||
class OpusAudioManager { |
|||
|
|||
companion object { |
|||
private const val TAG = "OpusAudioManager" |
|||
|
|||
// 音频数据分块发送的常量 |
|||
private const val AUDIO_CHUNK_SIZE = 120 // 每次发送120字节 |
|||
private const val AUDIO_SEND_INTERVAL = 60L // 发送间隔60ms |
|||
|
|||
// 重发机制相关常量 |
|||
private const val MAX_RETRY_COUNT = 1 // 最大重试次数 |
|||
private const val RETRY_DELAY = 30L // 重试延迟时间(毫秒) |
|||
} |
|||
|
|||
// ====================================================================================================== |
|||
// 回调接口定义 |
|||
// ====================================================================================================== |
|||
|
|||
/** |
|||
* Opus音频管理器回调接口 |
|||
*/ |
|||
interface OpusAudioCallback { |
|||
/** |
|||
* 音频数据解码完成回调 |
|||
* @param data 解码后的音频数据 |
|||
* @param channel 音频通道数 |
|||
*/ |
|||
fun onAudioDataDecoded(data: ByteArray, channel: Int) |
|||
|
|||
/** |
|||
* 音频数据编码完成回调 |
|||
* @param data 编码后的音频数据 |
|||
*/ |
|||
fun onAudioDataEncoded(data: ByteArray) |
|||
|
|||
/** |
|||
* 解码流状态变化回调 |
|||
* @param isStarted 是否已启动 |
|||
*/ |
|||
fun onDecodeStreamStateChanged(isStarted: Boolean) |
|||
|
|||
/** |
|||
* 编码流状态变化回调 |
|||
* @param isStarted 是否已启动 |
|||
*/ |
|||
fun onEncodeStreamStateChanged(isStarted: Boolean) |
|||
|
|||
/** |
|||
* 错误回调 |
|||
* @param errorType 错误类型 (decode/encode) |
|||
* @param code 错误代码 |
|||
* @param message 错误信息 |
|||
*/ |
|||
fun onError(errorType: String, code: Int, message: String?) |
|||
} |
|||
|
|||
// ====================================================================================================== |
|||
// 成员变量 |
|||
// ====================================================================================================== |
|||
|
|||
// Opus解码器实例 |
|||
private var opusManager: OpusManager? = null |
|||
private var option: OpusOption? = null |
|||
|
|||
private var opusManager1: OpusManager? = null |
|||
private var option1: OpusOption? = null |
|||
private val mainHandler = Handler(Looper.getMainLooper()) |
|||
|
|||
// 解码音频数据队列和处理线程 |
|||
private val audioDataQueue = LinkedBlockingQueue<ByteArray>() |
|||
private var audioQueueProcessorThread: Thread? = null |
|||
|
|||
// 音频数据缓存 |
|||
private val audioDataBuffer = mutableListOf<Byte>() |
|||
|
|||
// 音频数据发送相关 |
|||
private val audioSendQueue = LinkedBlockingQueue<ByteArray>() |
|||
private var audioSendThread: Thread? = null |
|||
private val audioSendHandler = Handler(Looper.getMainLooper()) |
|||
private val isAudioSending = AtomicBoolean(false) |
|||
|
|||
// 音频数据缓冲区,用于累积数据到指定字节再发送 |
|||
private val audioBuffer = mutableListOf<Byte>() |
|||
|
|||
// 回调接口 |
|||
private var callback: OpusAudioCallback? = null |
|||
|
|||
// 初始化状态 |
|||
private var isInitialized = false |
|||
|
|||
// ====================================================================================================== |
|||
// 初始化和配置方法 |
|||
// ====================================================================================================== |
|||
|
|||
/** |
|||
* 初始化Opus音频管理器 |
|||
* @return 是否初始化成功 |
|||
*/ |
|||
fun initialize(): Boolean { |
|||
if (isInitialized) return true |
|||
|
|||
try { |
|||
// 初始化OpusManager和OpusOption |
|||
opusManager = OpusManager() |
|||
option = OpusOption() |
|||
|
|||
opusManager1 = OpusManager() |
|||
option1 = OpusOption() |
|||
|
|||
// 启动队列处理 |
|||
startAudioQueueProcessing() |
|||
|
|||
isInitialized = true |
|||
Log.d(TAG, "OpusAudioManager初始化成功") |
|||
return true |
|||
} catch (e: OpusException) { |
|||
Log.e(TAG, "OpusAudioManager初始化失败: ${e.message}", e) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置回调接口 |
|||
* @param callback 回调接口实例 |
|||
*/ |
|||
fun setCallback(callback: OpusAudioCallback?) { |
|||
this.callback = callback |
|||
} |
|||
|
|||
/** |
|||
* 清理资源 |
|||
*/ |
|||
fun cleanup() { |
|||
Log.d(TAG, "开始清理OpusAudioManager资源") |
|||
|
|||
// 停止编解码流 |
|||
stopDecodeStream() |
|||
stopEncodeStream() |
|||
|
|||
// 停止音频发送线程 |
|||
stopAudioSendThread() |
|||
|
|||
// 停止音频队列处理线程 |
|||
audioQueueProcessorThread?.interrupt() |
|||
audioQueueProcessorThread = null |
|||
|
|||
// 清理缓存 |
|||
synchronized(audioDataBuffer) { |
|||
audioDataBuffer.clear() |
|||
} |
|||
synchronized(audioBuffer) { |
|||
audioBuffer.clear() |
|||
} |
|||
audioDataQueue.clear() |
|||
audioSendQueue.clear() |
|||
|
|||
// 清理Opus相关资源 |
|||
opusManager = null |
|||
option = null |
|||
opusManager1 = null |
|||
option1 = null |
|||
callback = null |
|||
|
|||
isInitialized = false |
|||
Log.d(TAG, "OpusAudioManager资源清理完成") |
|||
} |
|||
|
|||
// ====================================================================================================== |
|||
// Opus解码相关方法 |
|||
// ====================================================================================================== |
|||
|
|||
/** |
|||
* 开始Opus数据流解码 |
|||
* @param hasHeader 是否包含文件头,通常BLE传输的Opus没有文件头 |
|||
* @param channel 音频通道数 |
|||
* @param sampleRate 采样率,确认设备端Opus编码采样率 |
|||
* @param packetSize 包大小,确认设备端Opus编码帧长 |
|||
* @return 是否成功开始解码 |
|||
*/ |
|||
fun startDecodeStream( |
|||
hasHeader: Boolean = false, |
|||
channel: Int = 1, |
|||
sampleRate: Int = 16000, |
|||
packetSize: Int = 40 |
|||
): Boolean { |
|||
if (!isInitialized || opusManager == null) { |
|||
Log.e(TAG, "OpusManager未初始化,无法开始解码") |
|||
return false |
|||
} |
|||
|
|||
// 如果已经在解码流,先停止 |
|||
if (opusManager?.isDecodeStream == true) { |
|||
opusManager?.stopDecodeStream() |
|||
Log.d(TAG, "已停止正在进行的Opus解码流") |
|||
} |
|||
|
|||
// 清理音频数据缓存,确保开始时是干净的状态 |
|||
synchronized(audioDataBuffer) { |
|||
audioDataBuffer.clear() |
|||
Log.d(TAG, "开始解码前已清理音频数据缓存") |
|||
} |
|||
|
|||
// 配置解码参数 |
|||
option?.apply { |
|||
setHasHead(hasHeader) |
|||
setChannel(channel) |
|||
setSampleRate(sampleRate) |
|||
setPacketSize(packetSize) |
|||
} |
|||
|
|||
Log.d(TAG, "准备开始Opus数据流解码, 参数: hasHeader=$hasHeader, channel=$channel, sampleRate=$sampleRate, packetSize=$packetSize") |
|||
|
|||
try { |
|||
opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { |
|||
override fun onDecodeStream(data: ByteArray?) { |
|||
if (data != null) { |
|||
val channelCount = option?.getChannel() ?: 1 |
|||
if (channelCount >= 1) { |
|||
callback?.onAudioDataDecoded(data, channelCount) |
|||
} else { |
|||
// Log.e(TAG, "Opus解码数据错误: ${data.size} bytes") |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun onStart() { |
|||
Log.i(TAG, "Opus数据流解码已开始") |
|||
callback?.onDecodeStreamStateChanged(true) |
|||
} |
|||
|
|||
override fun onComplete(outPath: String?) { |
|||
Log.i(TAG, "Opus数据流解码完成: $outPath") |
|||
callback?.onDecodeStreamStateChanged(false) |
|||
} |
|||
|
|||
override fun onError(code: Int, message: String?) { |
|||
Log.e(TAG, "Opus数据流解码错误: [$code] $message") |
|||
callback?.onError("decode", code, message) |
|||
callback?.onDecodeStreamStateChanged(false) |
|||
} |
|||
}) |
|||
|
|||
return true |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "启动Opus解码流失败: ${e.message}", e) |
|||
callback?.onError("decode", -1, e.message) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 停止Opus数据流解码 |
|||
* @return 是否成功停止解码 |
|||
*/ |
|||
fun stopDecodeStream(): Boolean { |
|||
if (opusManager?.isDecodeStream == true) { |
|||
opusManager?.stopDecodeStream() |
|||
|
|||
// 清理音频数据缓存 |
|||
synchronized(audioDataBuffer) { |
|||
audioDataBuffer.clear() |
|||
Log.d(TAG, "已清理音频数据缓存") |
|||
} |
|||
|
|||
Log.i(TAG, "已停止Opus数据流解码") |
|||
callback?.onDecodeStreamStateChanged(false) |
|||
return true |
|||
} |
|||
Log.d(TAG, "Opus数据流未在解码或OpusManager未初始化") |
|||
return false |
|||
} |
|||
|
|||
/** |
|||
* 处理接收到的音频数据进行解码 |
|||
* @param data 音频数据 (Opus编码) |
|||
*/ |
|||
fun processAudioData(data: ByteArray) { |
|||
if (!isInitialized) { |
|||
Log.w(TAG, "OpusAudioManager未初始化,忽略音频数据") |
|||
return |
|||
} |
|||
|
|||
if (opusManager?.isDecodeStream == true) { |
|||
// 将数据加入队列进行异步处理 |
|||
audioDataQueue.offer(data) |
|||
} else { |
|||
Log.d(TAG, "Opus解码流未启动,忽略音频数据") |
|||
} |
|||
} |
|||
|
|||
// ====================================================================================================== |
|||
// Opus编码相关方法 |
|||
// ====================================================================================================== |
|||
|
|||
/** |
|||
* 开始Opus数据流编码 |
|||
* @return 是否成功开始编码 |
|||
*/ |
|||
fun startEncodeStream(): Boolean { |
|||
if (!isInitialized || opusManager1 == null) { |
|||
Log.e(TAG, "OpusManager未初始化,无法开始编码") |
|||
return false |
|||
} |
|||
|
|||
// 如果已经在编码流,先停止 |
|||
if (opusManager1?.isEncodeStream == true) { |
|||
Log.d(TAG, "Opus编码流已在运行") |
|||
return true |
|||
} |
|||
|
|||
// 配置解码参数 |
|||
option1?.apply { |
|||
setHasHead(false) |
|||
setChannel(1) |
|||
setSampleRate(16000) |
|||
setPacketSize(40) |
|||
} |
|||
try { |
|||
Log.d(TAG, "准备开始Opus数据流编码") |
|||
opusManager1?.startEncodeStream(object : OnEncodeStreamCallback { |
|||
override fun onEncodeStream(data: ByteArray?) { |
|||
Log.d(TAG, "Opus编码回调收到数据,大小: ${data?.size ?: 0} bytes") |
|||
if (data != null) { |
|||
|
|||
// 编码完成的数据处理: |
|||
// 1. 通知回调 |
|||
callback?.onAudioDataEncoded(data) |
|||
|
|||
// // 2. 将编码后的数据加入发送队列进行分块发送 |
|||
// addAudioDataToSendQueue(data) |
|||
} else { |
|||
Log.w(TAG, "编码回调收到空数据") |
|||
} |
|||
} |
|||
|
|||
override fun onStart() { |
|||
Log.i(TAG, "Opus数据流编码已开始") |
|||
callback?.onEncodeStreamStateChanged(true) |
|||
} |
|||
|
|||
override fun onComplete(outPath: String?) { |
|||
Log.i(TAG, "Opus数据流编码完成: $outPath") |
|||
callback?.onEncodeStreamStateChanged(false) |
|||
} |
|||
|
|||
override fun onError(code: Int, message: String?) { |
|||
Log.e(TAG, "Opus数据流编码错误: [$code] $message") |
|||
callback?.onError("encode", code, message) |
|||
callback?.onEncodeStreamStateChanged(false) |
|||
} |
|||
}) |
|||
|
|||
return true |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "启动Opus编码流失败: ${e.message}", e) |
|||
callback?.onError("encode", -1, e.message) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 停止Opus数据流编码 |
|||
* @return 是否成功停止编码 |
|||
*/ |
|||
fun stopEncodeStream(): Boolean { |
|||
if (opusManager1?.isEncodeStream == true) { |
|||
opusManager1?.stopEncodeStream() |
|||
stopAudioSendThread() // 停止音频发送线程 |
|||
Log.i(TAG, "已停止Opus数据流编码") |
|||
callback?.onEncodeStreamStateChanged(false) |
|||
return true |
|||
} |
|||
Log.d(TAG, "Opus数据流未在编码或OpusManager未初始化") |
|||
return false |
|||
} |
|||
|
|||
/** |
|||
* 写入外部音频数据到Opus编码流 |
|||
* 该函数用于将外部音频数据(如麦克风录音)写入到Opus编码器中 |
|||
* |
|||
* @param data 音频数据字节数组,通常为PCM格式 |
|||
*/ |
|||
fun writeExternalAudioData(data: ByteArray) { |
|||
if (!isInitialized || opusManager1 == null) { |
|||
Log.e(TAG, "OpusManager未初始化") |
|||
return |
|||
} |
|||
|
|||
// 如果已经在编码流中,直接写入数据 |
|||
if (opusManager1?.isEncodeStream == true) { |
|||
Log.d(TAG, "正在进行Opus编码流,写入音频数据") |
|||
// 将外部音频数据写入编码流 |
|||
opusManager1?.writeEncodeStream(data) |
|||
} else { |
|||
Log.w(TAG, "Opus编码流未启动,无法写入音频数据") |
|||
} |
|||
} |
|||
|
|||
// ====================================================================================================== |
|||
// 状态查询方法 |
|||
// ====================================================================================================== |
|||
|
|||
/** |
|||
* 检查是否正在解码 |
|||
* @return 是否正在解码 |
|||
*/ |
|||
fun isDecoding(): Boolean { |
|||
return opusManager?.isDecodeStream == true |
|||
} |
|||
|
|||
/** |
|||
* 检查是否正在编码 |
|||
* @return 是否正在编码 |
|||
*/ |
|||
fun isEncoding(): Boolean { |
|||
return opusManager1?.isEncodeStream == true |
|||
} |
|||
|
|||
/** |
|||
* 检查是否已初始化 |
|||
* @return 是否已初始化 |
|||
*/ |
|||
fun isInitialized(): Boolean { |
|||
return isInitialized |
|||
} |
|||
|
|||
// ====================================================================================================== |
|||
// 私有方法 - 音频数据处理 |
|||
// ====================================================================================================== |
|||
|
|||
|
|||
/** |
|||
* 停止音频发送线程 |
|||
*/ |
|||
private fun stopAudioSendThread() { |
|||
isAudioSending.set(false) |
|||
audioSendThread?.interrupt() |
|||
|
|||
try { |
|||
audioSendThread?.join(1000) // 等待最多1秒 |
|||
} catch (e: InterruptedException) { |
|||
Log.w(TAG, "等待音频发送线程停止时被中断") |
|||
} |
|||
|
|||
audioSendThread = null |
|||
audioSendQueue.clear() |
|||
|
|||
synchronized(audioBuffer) { |
|||
audioBuffer.clear() |
|||
} |
|||
|
|||
Log.d(TAG, "音频发送线程已停止") |
|||
} |
|||
|
|||
/** |
|||
* 启动音频队列处理线程 |
|||
*/ |
|||
private fun startAudioQueueProcessing() { |
|||
audioQueueProcessorThread = Thread { |
|||
Log.d(TAG, "音频队列处理线程已启动") |
|||
|
|||
while (!Thread.currentThread().isInterrupted) { |
|||
try { |
|||
// 从队列中取出音频数据进行处理 |
|||
val audioData = audioDataQueue.take() // 阻塞等待 |
|||
|
|||
// 将音频数据写入Opus解码流 |
|||
opusManager?.writeAudioStream(audioData) |
|||
|
|||
} catch (e: InterruptedException) { |
|||
Log.d(TAG, "音频队列处理线程被中断") |
|||
break |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "音频队列处理异常: ${e.message}", e) |
|||
} |
|||
} |
|||
|
|||
Log.d(TAG, "音频队列处理线程已停止") |
|||
} |
|||
|
|||
audioQueueProcessorThread?.start() |
|||
} |
|||
} |
|||
@ -0,0 +1,160 @@ |
|||
package com.yunqiinnovation.ble_service |
|||
|
|||
import android.content.Context |
|||
import android.util.Log |
|||
import java.io.File |
|||
import java.io.FileOutputStream |
|||
import java.text.SimpleDateFormat |
|||
import java.util.Date |
|||
import java.util.Locale |
|||
import java.util.concurrent.LinkedBlockingQueue |
|||
import java.util.concurrent.atomic.AtomicBoolean |
|||
|
|||
/** |
|||
* 录音文件管理类:负责音频数据的异步写入和文件管理 |
|||
*/ |
|||
class RecordingFile(private val context: Context) { |
|||
private var currentAudioFile: File? = null |
|||
private var fos: FileOutputStream? = null |
|||
|
|||
// 新增:用于异步写入的队列和线程 |
|||
private val writeQueue = LinkedBlockingQueue<ByteArray>() |
|||
private val isWriting = AtomicBoolean(false) |
|||
private var writeThread: Thread? = null |
|||
|
|||
// 用于存储音频数据的缓冲区 |
|||
private val dataBuffer = mutableListOf<ByteArray>() |
|||
private var totalBytesWritten = 0 |
|||
|
|||
var fileName = "" |
|||
internal fun creatingFiles() { |
|||
if (fos != null || currentAudioFile != null) { |
|||
return |
|||
} |
|||
// 创建新的音频文件 |
|||
val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) |
|||
val timestamp = dateFormat.format(Date()) |
|||
|
|||
val filePath = |
|||
File(context.getExternalFilesDir(null), "${fileName}_${timestamp}.txt").absolutePath |
|||
currentAudioFile = File(filePath) |
|||
currentAudioFile?.createNewFile() |
|||
// 追加音频数据到文件 |
|||
fos = FileOutputStream(currentAudioFile, true) |
|||
startWriteThread() // 新增:启动写入线程 |
|||
Log.d("TAG", "创建文件: ${currentAudioFile?.absolutePath}") |
|||
} |
|||
|
|||
// 新增:启动写入线程 |
|||
private fun startWriteThread() { |
|||
if (isWriting.get()) return |
|||
isWriting.set(true) |
|||
writeThread = Thread { |
|||
try { |
|||
while (isWriting.get() || writeQueue.isNotEmpty()) { |
|||
val data = writeQueue.poll() ?: continue |
|||
fos?.write(data) |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e("", "异步写入音频数据失败: ${e.message}") |
|||
} |
|||
} |
|||
writeThread?.start() |
|||
} |
|||
|
|||
/** |
|||
* 保存音频数据到 opus 文件(异步) |
|||
* @param buffer 音频数据字节数组 |
|||
*/ |
|||
internal fun saveAudioDataToWav(buffer: ByteArray) { |
|||
if (fos == null || currentAudioFile == null) return |
|||
// 放入队列,由写线程写入 |
|||
// // 1. 将字节数据转换为十六进制字符串 |
|||
// val hexData = buildString { |
|||
// buffer.forEachIndexed { index, byte -> |
|||
// append("%02X".format(byte)) |
|||
|
|||
// } |
|||
// } |
|||
|
|||
// 2. 写入十六进制字符串 |
|||
|
|||
writeQueue.offer(buffer) |
|||
totalBytesWritten += buffer.size |
|||
} |
|||
|
|||
/** |
|||
* 保存日志信息到文件(重载方法) |
|||
* 使用UTF-8编码处理字符串 |
|||
* @param logMessage 日志消息字符串 |
|||
*/ |
|||
internal fun saveAudioDataToWav(logMessage: String) { |
|||
if (fos == null || currentAudioFile == null) return |
|||
try { |
|||
// 使用UTF-8编码将字符串转换为字节数组,并添加换行符 |
|||
val logBytes = (logMessage + "\n").toByteArray(Charsets.UTF_8) |
|||
writeQueue.offer(logBytes) |
|||
totalBytesWritten += logBytes.size |
|||
Log.d("RecordingFile", "保存日志信息: ${logMessage.length} 字符, ${logBytes.size} 字节 (UTF-8)") |
|||
} catch (e: Exception) { |
|||
Log.e("RecordingFile", "UTF-8编码转换失败: ${e.message}") |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将音频数据转换为UTF-8编码的十六进制字符串格式并保存 |
|||
* @param buffer 音频数据字节数组 |
|||
* @param addTimestamp 是否添加时间戳,默认为true |
|||
*/ |
|||
internal fun saveAudioDataAsUtf8Hex(buffer: ByteArray, addTimestamp: Boolean = true) { |
|||
if (fos == null || currentAudioFile == null) return |
|||
|
|||
try { |
|||
val timestamp = if (addTimestamp) { |
|||
"[${SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())}] " |
|||
} else "" |
|||
|
|||
// 将字节数组转换为十六进制字符串 |
|||
val hexString = buffer.joinToString(" ") { "%02X".format(it) } |
|||
val dataLine = "${timestamp}音频数据 (${buffer.size} 字节): $hexString\n" |
|||
|
|||
// 使用UTF-8编码转换为字节数组 |
|||
val utf8Bytes = dataLine.toByteArray(Charsets.UTF_8) |
|||
writeQueue.offer(utf8Bytes) |
|||
totalBytesWritten += utf8Bytes.size |
|||
|
|||
Log.d("RecordingFile", "保存UTF-8格式音频数据: ${buffer.size} 字节 -> ${utf8Bytes.size} UTF-8字节") |
|||
} catch (e: Exception) { |
|||
Log.e("RecordingFile", "UTF-8格式保存失败: ${e.message}") |
|||
} |
|||
} |
|||
|
|||
|
|||
internal fun closeFile() { |
|||
try { |
|||
if (fos == null || currentAudioFile == null) { |
|||
return |
|||
} |
|||
isWriting.set(false) |
|||
writeThread?.join(500) // 等待写线程结束 |
|||
fos?.close() |
|||
|
|||
// 更新WAV文件头 |
|||
currentAudioFile?.let { file -> |
|||
// 如果文件小于44字节,直接删除 |
|||
if (file.length() <= 0) { |
|||
file.delete() |
|||
Log.d("TAG", "音频文件过小已删除: ${file.absolutePath}") |
|||
} else { |
|||
Log.d("TAG", "音频文件保存完成: ${file.absolutePath}") |
|||
} |
|||
} |
|||
|
|||
} catch (e: Exception) { |
|||
Log.e("TAG", "更新opus失败: ${e.message}") |
|||
} finally { |
|||
fos = null |
|||
currentAudioFile = null |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue