Browse Source

上传ble 指令更新

newdev_shunjiawei
liwei1dao 4 months ago
parent
commit
fbf00b8584
  1. 1
      android/gradle.properties
  2. 3
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  3. 3
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  4. 47
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt
  5. 6
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt
  6. 124
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  7. 14
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  8. 3
      local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt
  9. 558
      pubspec.lock

1
android/gradle.properties

@ -1,4 +1,5 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.java.home=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
android.useAndroidX=true
android.enableJetifier=true
android.max.page.size=16384

3
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -206,6 +206,9 @@ object BleAgent : BleService.Callback, AgentServiceListener {
override fun onWakeupVoiceUpgradeFailed() {
}
override fun onCallSleepReceived(channel: Int) {
}
//============================================================================================

3
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -2067,6 +2067,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
override fun onWakeupVoiceUpgradeFailed() {
}
override fun onCallSleepReceived(channel: Int) {
}
// TODO: 双端翻译时启用副芯片回调
// override fun onSecondaryChipConnectionStateChangedDetail(state: Int, deviceAddress: String?, reason: String?) { }
// override fun onSecondaryChipConnectionsCountChanged(count: Int, addresses: List<String>) { }

47
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt

@ -86,9 +86,10 @@ class BleCommandSender {
fun notifyWakeupSignalReceived()
/**
* 通知通话休眠指令接收
* 通知声道通话休眠指令接收
* @param channel 休眠声道:BleConst.AUDIO_CHANNEL_LEFT(左) / BleConst.AUDIO_CHANNEL_RIGHT(右)
*/
fun notifyCallSleepReceived()
fun notifyCallSleepReceived(channel: Int)
/**
* 通知开始 AI 单次对话
@ -382,7 +383,8 @@ class BleCommandSender {
BleConst.CODEC_CONTROL_A2DP_PLAY -> "A2DP播放模式"
BleConst.CODEC_CONTROL_CALL_RECORD_PLAY -> "通话记录播放模式"
BleConst.CODEC_CONTROL_ENCODE_ON -> "已打开编码"
BleConst.CODEC_CONTROL_CALL_Sleep -> "通话休眠"
BleConst.CODEC_CONTROL_CALL_SLEEP_LEFT -> "左声道通话休眠"
BleConst.CODEC_CONTROL_CALL_SLEEP_RIGHT -> "右声道通话休眠"
else -> "未知状态($codecStatus)"
}
@ -506,18 +508,18 @@ class BleCommandSender {
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
// // 计算CRC校验 - 设备对帧头(0xCC)之后、CRC之前的字节计算,不含帧头本身
// val payload = data.sliceArray(1 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
}
// 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)
@ -589,16 +591,23 @@ class BleCommandSender {
}
BleConst.CMD_CONTROL_CODEC -> {
// 设备主动上报编解码控制状态,目前只关心 CODEC_CONTROL_CALL_Sleep (0xFF)
// 设备主动上报编解码控制状态,关心左/右声道通话休眠 (0xF0 / 0xF1)
// 帧格式: 0xCC 0x05 [len] [codecStatus, channelMode, ...] [crc]
val codecStatus =
if (notifyData.isNotEmpty()) notifyData[0].toInt() and 0xFF else -1
if (codecStatus == BleConst.CODEC_CONTROL_CALL_Sleep) {
Log.i(TAG, "收到主动上报: 通话休眠 (codecStatus=0xFF)")
callback?.notifyCallSleepReceived()
} else {
when (codecStatus) {
BleConst.CODEC_CONTROL_CALL_SLEEP_LEFT -> {
Log.i(TAG, "收到主动上报: 左声道通话休眠 (codecStatus=0xF0)")
callback?.notifyCallSleepReceived(BleConst.AUDIO_CHANNEL_LEFT)
}
BleConst.CODEC_CONTROL_CALL_SLEEP_RIGHT -> {
Log.i(TAG, "收到主动上报: 右声道通话休眠 (codecStatus=0xF1)")
callback?.notifyCallSleepReceived(BleConst.AUDIO_CHANNEL_RIGHT)
}
else -> {
Log.d(TAG, "收到主动上报编解码控制: codecStatus=0x${codecStatus.toString(16)}")
}
}
val resultMap = mapOf(
"success" to true,
"command" to infoType,

6
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt

@ -121,8 +121,10 @@ object BleConst {
const val CODEC_CONTROL_A2DP_PLAY = 0xA2
/** mic和dac(音乐或者通话远端)声音 */
const val CODEC_CONTROL_CALL_RECORD_PLAY = 0xA3
/** 通话休眠 */
const val CODEC_CONTROL_CALL_Sleep = 0xFF
/** 左声道通话休眠(休眠期间暂停下发左声道,仅传输右声道,固定 1s 后自动恢复) */
const val CODEC_CONTROL_CALL_SLEEP_LEFT = 0xF0
/** 右声道通话休眠(休眠期间暂停下发右声道,仅传输左声道,固定 1s 后自动恢复) */
const val CODEC_CONTROL_CALL_SLEEP_RIGHT = 0xF1
/** 打开编码指令 */
const val CODEC_CONTROL_ENCODE_ON = 0xB1
/** 左声道 */

124
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -71,6 +71,9 @@ object BleService {
// 升级唤醒词失败相关回调
fun onWakeupVoiceUpgradeFailed()
// 声道通话休眠相关回调
fun onCallSleepReceived(channel: Int)
}
// ======================================================================================================
@ -141,10 +144,23 @@ object BleService {
// 音频数据分块发送的常量
private val AUDIO_CHUNK_SIZE = 120 // 每次发送120字节
private const val AUDIO_SEND_INTERVAL_NORMAL = 60L // 正常发送间隔60ms
private const val AUDIO_SEND_INTERVAL_SLEEP = 1000L // 收到 CODEC_CONTROL_CALL_Sleep 后插入的单次暂停
// 收到 CODEC_CONTROL_CALL_Sleep 时置 true,下一次发包前会消费此标记并 sleep 一次更长的间隔
private const val CHANNEL_SLEEP_DURATION = 400L // 收到 F0/F1 后该声道固定休眠 1s,到期自动恢复
// 左/右声道休眠到期时间戳(SystemClock.elapsedRealtime 基准,0 表示未休眠)。
// 收到 F0(左)/F1(右) 时置为 now + CHANNEL_SLEEP_DURATION,
// 休眠窗口内该声道的包暂不发送(转入积压缓存),仅传输另一声道,到期后按序补发,不丢包。
@Volatile
private var leftChannelSleepUntil: Long = 0L
@Volatile
private var pendingCallSleep: Boolean = false
private var rightChannelSleepUntil: Long = 0L
// 声道休眠期间该声道的包不丢弃,转入对应积压缓存,待声道唤醒后按 FIFO 顺序补发。
// 仅在音频发送线程内访问,无需额外加锁。
private val leftHoldBuffer = ArrayDeque<ByteArray>()
private val rightHoldBuffer = ArrayDeque<ByteArray>()
// 积压缓存安全上限(防止设备持续上报休眠导致内存无限增长)。
// 1s 休眠约积压 16 包,留足余量;超限时丢弃最旧包并告警(仅在异常场景触发)。
private const val CHANNEL_HOLD_BUFFER_MAX = 200
// 左右积压同时可补发时的轮转标记,保证两声道公平补发、避免一方饿死。
private var holdDrainPreferLeft = true
// 音频数据缓冲区,用于累积数据到80字节再发送
private val audioBuffer = mutableListOf<Byte>()
// 重发机制相关常量
@ -349,11 +365,11 @@ object BleService {
}
/**
* 通知通话休眠指令接收
* 通知声道通话休眠指令接收
*/
override fun notifyCallSleepReceived() {
// 使用外部类的方法来分发通话休眠指令,避免无限递归
this@BleService.notifyCallSleepReceived()
override fun notifyCallSleepReceived(channel: Int) {
// 使用外部类的方法来处理声道通话休眠指令,避免无限递归
this@BleService.notifyCallSleepReceived(channel)
}
/**
@ -1350,11 +1366,34 @@ object BleService {
while (isAudioSending.get() && !Thread.currentThread().isInterrupted) {
try {
// 从队列中取出音频数据块
val audioChunk = audioSendQueue.poll(20, TimeUnit.MILLISECONDS)
// 1) 从主队列取新包,按声道分流到积压缓存。
// 合包布局:[4B 序号(大端)] + [1B 声道(0=左/1=右)] + [N × opus]
// 阻塞至多 20ms 等待新数据,避免空转;随后把队列里已到达的剩余包一次性分流。
audioSendQueue.poll(20, TimeUnit.MILLISECONDS)?.let { first ->
routeToHoldBuffer(first)
while (true) {
val more = audioSendQueue.poll() ?: break
routeToHoldBuffer(more)
}
}
// 2) 选出一包可发送的数据:声道已唤醒(不在休眠窗口)且缓存非空。
// 左右都可发时按 holdDrainPreferLeft 轮转,保证两声道公平补发、避免一方饿死。
val now = SystemClock.elapsedRealtime()
val leftReady = leftHoldBuffer.isNotEmpty() && now >= leftChannelSleepUntil
val rightReady = rightHoldBuffer.isNotEmpty() && now >= rightChannelSleepUntil
val audioChunk: ByteArray? = when {
leftReady && rightReady -> {
holdDrainPreferLeft = !holdDrainPreferLeft
if (holdDrainPreferLeft) leftHoldBuffer.removeFirst()
else rightHoldBuffer.removeFirst()
}
leftReady -> leftHoldBuffer.removeFirst()
rightReady -> rightHoldBuffer.removeFirst()
else -> null // 两声道都在休眠或都无积压,本轮不发送
}
if (audioChunk != null) {
// 合包布局:[4B 序号(大端)] + [1B 声道(0=左/1=右)] + [N × opus]
// 调试录音文件只保留拼接后的纯 opus 数据,剥离 5 字节包头再写入
if (audioChunk.size > 5) {
val payload = audioChunk.copyOfRange(5, audioChunk.size)
@ -1371,16 +1410,11 @@ object BleService {
OpusAudioManager.CHANNEL_RIGHT -> "R"
else -> "?(${audioChunk[4]})"
}
Log.i(TAG, "音频下行发送 ch=$chTag seq=$seq size=${audioChunk.size}B queue=${audioSendQueue.size}")
Log.i(TAG, "音频下行发送 ch=$chTag seq=$seq size=${audioChunk.size}B " +
"hold(L=${leftHoldBuffer.size},R=${rightHoldBuffer.size}) queue=${audioSendQueue.size}")
}
sendAudioChunkWithCallback(audioChunk)
// 若刚收到通话休眠主动上报,则把下一次间隔替换为 SLEEP 间隔,
// 消费后立即清标记,确保只插入一次较长暂停
val intervalToUse = if (pendingCallSleep) {
pendingCallSleep = false
AUDIO_SEND_INTERVAL_SLEEP
} else AUDIO_SEND_INTERVAL_NORMAL
Thread.sleep(intervalToUse)
Thread.sleep(AUDIO_SEND_INTERVAL_NORMAL)
}
} catch (e: InterruptedException) {
Log.d(TAG, "音频发送线程被中断")
@ -1396,6 +1430,25 @@ object BleService {
}
}
/**
* 将一包数据按其声道标记分流到对应的积压缓存。
* 休眠声道的包不会丢弃,先入缓存等唤醒后补发;非休眠声道的包同样入缓存,由发送线程统一按节拍取出。
* 仅作为安全阀:当某声道缓存超过 CHANNEL_HOLD_BUFFER_MAX 时丢弃最旧包并告警(正常 1s 休眠不会触发)。
*/
private fun routeToHoldBuffer(chunk: ByteArray) {
val channelTag = if (chunk.size >= 5) chunk[4] else null
val buffer = when (channelTag) {
OpusAudioManager.CHANNEL_RIGHT -> rightHoldBuffer
// 左声道及无有效声道标记(异常包)统一归入左缓存处理
else -> leftHoldBuffer
}
if (buffer.size >= CHANNEL_HOLD_BUFFER_MAX) {
buffer.removeFirst()
Log.w(TAG, "声道积压缓存超上限($CHANNEL_HOLD_BUFFER_MAX),丢弃最旧包 ch=${channelTag}")
}
buffer.addLast(chunk)
}
/**
* 停止音频数据发送线程
*/
@ -1404,6 +1457,10 @@ object BleService {
audioSendThread?.interrupt()
audioSendQueue.clear()
audioBuffer.clear() // 清空音频缓冲区
leftHoldBuffer.clear() // 清空左声道积压缓存
rightHoldBuffer.clear() // 清空右声道积压缓存
leftChannelSleepUntil = 0L
rightChannelSleepUntil = 0L
Log.i(TAG, "音频发送线程已停止,队列和缓冲区已清空")
}
@ -1939,13 +1996,32 @@ object BleService {
}
/**
* 收到设备主动上报的 CODEC_CONTROL_CALL_Sleep 时调用。
* 仅设置一个一次性标记,由音频发送线程在下一次发包时消费并执行一次更长的暂停,
* 之后自动回到正常 60ms 节拍。多次连续触发会折叠为单次暂停(满足"已在休眠,无需叠加")。
* 收到设备主动上报的左/右声道通话休眠 (F0/F1) 时调用。
* 将对应声道的休眠到期时间置为 now + CHANNEL_SLEEP_DURATION(1s),
* 休眠窗口内该声道的包转入积压缓存暂不发送、仅传输另一声道,到期后按序补发,不丢包。
* 重复触发只是刷新到期时间,不会叠加。
* @param channel BleConst.AUDIO_CHANNEL_LEFT(左) / BleConst.AUDIO_CHANNEL_RIGHT(右)
*/
private fun notifyCallSleepReceived() {
pendingCallSleep = true
Log.i(TAG, "收到通话休眠指令,下一帧发送间隔将切到 ${AUDIO_SEND_INTERVAL_SLEEP}ms")
private fun notifyCallSleepReceived(channel: Int) {
val until = SystemClock.elapsedRealtime() + CHANNEL_SLEEP_DURATION
when (channel) {
BleConst.AUDIO_CHANNEL_LEFT -> {
leftChannelSleepUntil = until
Log.i(TAG, "收到左声道休眠指令,暂停左声道下发 ${CHANNEL_SLEEP_DURATION}ms")
}
BleConst.AUDIO_CHANNEL_RIGHT -> {
rightChannelSleepUntil = until
Log.i(TAG, "收到右声道休眠指令,暂停右声道下发 ${CHANNEL_SLEEP_DURATION}ms")
}
else -> Log.w(TAG, "收到未知声道休眠指令: channel=$channel")
}
for (callback in callbacks) {
try {
callback.onCallSleepReceived(channel)
} catch (e: Exception) {
Log.e(TAG, "分发声道通话休眠回调异常", e)
}
}
}
/**

14
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt

@ -450,4 +450,18 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
// 发送升级失败事件
sendEvent(statusEventSink, resultMap, "发送升级失败异常")
}
override fun onCallSleepReceived(channel: Int) {
val channelName = when (channel) {
BleConst.AUDIO_CHANNEL_LEFT -> "左声道"
BleConst.AUDIO_CHANNEL_RIGHT -> "右声道"
else -> "未知声道($channel)"
}
Log.i(TAG, "[Flutter推送] 声道通话休眠: $channelName (channel=$channel)")
sendEvent(
statusEventSink,
mapOf("type" to "callSleep", "channel" to channel),
"发送声道通话休眠事件异常"
)
}
}

3
local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt

@ -412,4 +412,7 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
override fun onWakeupVoiceUpgradeFailed() {
}
override fun onCallSleepReceived(channel: Int) {
}
}

558
pubspec.lock

File diff suppressed because it is too large
Loading…
Cancel
Save