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