feat: 添加TTS流式处理和机器人重新定位功能
- 在MqttManager中实现TTS队列机制,支持流式文本的分句处理和顺序播放 - 添加机器人重新定位(repose)命令及状态监控 - 扩展NavController的goTo方法支持反向移动 - 通过TTS状态回调管理语音队列,避免语音重叠
This commit is contained in:
@@ -96,6 +96,9 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
}
|
||||
|
||||
override fun onTtsStatusChanged(ttsRequest: TtsRequest) {
|
||||
// Forward TTS status to MqttManager for stream queue handling
|
||||
mqttManager?.handleTtsStatusChange(ttsRequest)
|
||||
|
||||
when (ttsRequest.status) {
|
||||
TtsRequest.Status.STARTED -> {
|
||||
Log.i("MainActivity", "TTS started: ${ttsRequest.speech}")
|
||||
|
||||
@@ -8,6 +8,8 @@ import kotlinx.coroutines.*
|
||||
import org.eclipse.paho.client.mqttv3.*
|
||||
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
|
||||
import org.json.JSONObject
|
||||
import java.util.LinkedList
|
||||
import java.util.Queue
|
||||
|
||||
class MqttManager(
|
||||
private val context: Context,
|
||||
@@ -24,6 +26,13 @@ class MqttManager(
|
||||
private val scope = CoroutineScope(Dispatchers.IO + job)
|
||||
private var reconnectJob: Job? = null
|
||||
|
||||
// Streaming text buffer
|
||||
private val speechBuffer = StringBuilder()
|
||||
|
||||
// TTS Queue
|
||||
private val ttsQueue: Queue<TtsRequest> = LinkedList()
|
||||
private var isTtsBusy = false
|
||||
|
||||
init {
|
||||
try {
|
||||
mqttClient = MqttClient(brokerUri, clientId, MemoryPersistence())
|
||||
@@ -88,6 +97,7 @@ class MqttManager(
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
robot.removeTtsListener(this)
|
||||
scope.launch {
|
||||
try {
|
||||
reconnectJob?.cancel()
|
||||
@@ -161,8 +171,20 @@ class MqttManager(
|
||||
val lang = obj.optString("lang", "")
|
||||
speak(text, lang)
|
||||
}
|
||||
"stream" -> {
|
||||
val text = obj.optString("text", obj.optString("content", ""))
|
||||
val lang = obj.optString("lang", "")
|
||||
processStreamText(text, lang)
|
||||
}
|
||||
"repose" -> {
|
||||
val ok = navController.repose()
|
||||
speak("正在进行重新定位")
|
||||
Log.i(TAG, "Repose command sent: $ok")
|
||||
monitorReposeStatus()
|
||||
}
|
||||
"stop" -> {
|
||||
navController.stop()
|
||||
stopTts()
|
||||
}
|
||||
"patrol" -> {
|
||||
navController.randomPatrol()
|
||||
@@ -171,27 +193,137 @@ class MqttManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun goTo(location: String) {
|
||||
private fun processStreamText(text: String, langCode: String?) {
|
||||
speechBuffer.append(text)
|
||||
|
||||
while (true) {
|
||||
val content = speechBuffer.toString()
|
||||
var minIndex = -1
|
||||
|
||||
// Sentence delimiters: Chinese and English punctuation + newline
|
||||
val delimiters = listOf("。", "!", "?", "!", "?", "\n")
|
||||
|
||||
for (d in delimiters) {
|
||||
val index = content.indexOf(d)
|
||||
if (index != -1) {
|
||||
if (minIndex == -1 || index < minIndex) {
|
||||
minIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (minIndex != -1) {
|
||||
// Extract sentence including the delimiter
|
||||
val sentence = content.substring(0, minIndex + 1)
|
||||
speak(sentence, langCode)
|
||||
|
||||
// Remove processed sentence from buffer
|
||||
speechBuffer.delete(0, minIndex + 1)
|
||||
} else {
|
||||
// No more complete sentences found
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopTts() {
|
||||
// Clear buffer
|
||||
speechBuffer.setLength(0)
|
||||
|
||||
scope.launch(Dispatchers.Main) {
|
||||
ttsQueue.clear()
|
||||
isTtsBusy = false
|
||||
robot.cancelAllTtsRequests()
|
||||
Log.i(TAG, "TTS stopped and queue cleared.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun monitorReposeStatus() {
|
||||
scope.launch {
|
||||
Log.i(TAG, "Starting repose monitoring...")
|
||||
val timeout = 30000L
|
||||
val startTime = System.currentTimeMillis()
|
||||
var lastStatus: String? = null
|
||||
|
||||
while (System.currentTimeMillis() - startTime < timeout) {
|
||||
delay(1000L)
|
||||
val status = robot.reposeStatus.toString().uppercase()
|
||||
|
||||
if (status != lastStatus) {
|
||||
Log.i(TAG, "Repose status: $status")
|
||||
lastStatus = status
|
||||
}
|
||||
|
||||
if (status == "COMPLETE") {
|
||||
speak("重新定位成功", null)
|
||||
return@launch
|
||||
} else if (status == "FAILURE" || status == "ABORTED") {
|
||||
speak("重新定位失败", null)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
Log.w(TAG, "Repose monitoring timed out.")
|
||||
speak("重新定位超时", null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun goTo(location: String, backwards: Boolean = false) {
|
||||
val target = location.trim()
|
||||
if (target.isEmpty()) {
|
||||
Log.w(TAG, "GoTo ignored: empty location")
|
||||
return
|
||||
}
|
||||
val ok = navController.goTo(target)
|
||||
Log.i(TAG, "GoTo command sent: $target, result=$ok")
|
||||
val ok = navController.goTo(target, backwards)
|
||||
Log.i(TAG, "GoTo command sent: $target, backwards=$backwards, result=$ok")
|
||||
}
|
||||
|
||||
private fun speak(text: String, langCode: String?) {
|
||||
val content = text.trim()
|
||||
if (content.isEmpty()) {
|
||||
Log.w(TAG, "Speak ignored: empty text")
|
||||
// Log.w(TAG, "Speak ignored: empty text") // Too noisy for stream?
|
||||
return
|
||||
}
|
||||
val language = resolveLanguage(langCode)
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val ttsRequest = TtsRequest.create(content, false, language = language)
|
||||
|
||||
if (!isTtsBusy) {
|
||||
isTtsBusy = true
|
||||
robot.speak(ttsRequest)
|
||||
Log.i(TAG, "Speak command sent: $content, lang=$language")
|
||||
Log.i(TAG, "Speak immediate: $content")
|
||||
} else {
|
||||
ttsQueue.offer(ttsRequest)
|
||||
Log.i(TAG, "Speak queued: $content. Queue size: ${ttsQueue.size}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleTtsStatusChange(ttsRequest: TtsRequest) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
when (ttsRequest.status) {
|
||||
TtsRequest.Status.STARTED -> {
|
||||
isTtsBusy = true
|
||||
}
|
||||
TtsRequest.Status.COMPLETED,
|
||||
TtsRequest.Status.CANCELED,
|
||||
TtsRequest.Status.ERROR,
|
||||
TtsRequest.Status.ABORTED -> {
|
||||
isTtsBusy = false
|
||||
processNextTts()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processNextTts() {
|
||||
if (!isTtsBusy && ttsQueue.isNotEmpty()) {
|
||||
val next = ttsQueue.poll()
|
||||
if (next != null) {
|
||||
isTtsBusy = true
|
||||
robot.speak(next)
|
||||
Log.i(TAG, "Speak from queue: ${next.speech}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import com.robotemi.sdk.Robot
|
||||
class NavController(private val robot: Robot) {
|
||||
private val TAG = "NavController"
|
||||
|
||||
fun goTo(location: String): Boolean {
|
||||
robot.goTo(location)
|
||||
fun goTo(location: String, backwards: Boolean = false): Boolean {
|
||||
robot.goTo(location, backwards)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ class NavController(private val robot: Robot) {
|
||||
robot.patrol(locations, nonStop, times, waiting)
|
||||
}
|
||||
|
||||
fun repose(): Boolean {
|
||||
robot.repose()
|
||||
return true
|
||||
}
|
||||
|
||||
fun randomPatrol() {
|
||||
val allLocations = getAllLocations()
|
||||
if (allLocations.size < 3) {
|
||||
|
||||
Reference in New Issue
Block a user