feat: 添加TTS流式处理和机器人重新定位功能

- 在MqttManager中实现TTS队列机制,支持流式文本的分句处理和顺序播放
- 添加机器人重新定位(repose)命令及状态监控
- 扩展NavController的goTo方法支持反向移动
- 通过TTS状态回调管理语音队列,避免语音重叠
This commit is contained in:
2026-03-11 16:35:00 +08:00
parent 8c687aa76e
commit c3a37123c6
3 changed files with 148 additions and 8 deletions

View File

@@ -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}")

View File

@@ -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)
robot.speak(ttsRequest)
Log.i(TAG, "Speak command sent: $content, lang=$language")
if (!isTtsBusy) {
isTtsBusy = true
robot.speak(ttsRequest)
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}")
}
}
}

View File

@@ -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) {