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) { override fun onTtsStatusChanged(ttsRequest: TtsRequest) {
// Forward TTS status to MqttManager for stream queue handling
mqttManager?.handleTtsStatusChange(ttsRequest)
when (ttsRequest.status) { when (ttsRequest.status) {
TtsRequest.Status.STARTED -> { TtsRequest.Status.STARTED -> {
Log.i("MainActivity", "TTS started: ${ttsRequest.speech}") 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.*
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
import org.json.JSONObject import org.json.JSONObject
import java.util.LinkedList
import java.util.Queue
class MqttManager( class MqttManager(
private val context: Context, private val context: Context,
@@ -24,6 +26,13 @@ class MqttManager(
private val scope = CoroutineScope(Dispatchers.IO + job) private val scope = CoroutineScope(Dispatchers.IO + job)
private var reconnectJob: Job? = null 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 { init {
try { try {
mqttClient = MqttClient(brokerUri, clientId, MemoryPersistence()) mqttClient = MqttClient(brokerUri, clientId, MemoryPersistence())
@@ -88,6 +97,7 @@ class MqttManager(
} }
fun disconnect() { fun disconnect() {
robot.removeTtsListener(this)
scope.launch { scope.launch {
try { try {
reconnectJob?.cancel() reconnectJob?.cancel()
@@ -161,8 +171,20 @@ class MqttManager(
val lang = obj.optString("lang", "") val lang = obj.optString("lang", "")
speak(text, 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" -> { "stop" -> {
navController.stop() navController.stop()
stopTts()
} }
"patrol" -> { "patrol" -> {
navController.randomPatrol() 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() val target = location.trim()
if (target.isEmpty()) { if (target.isEmpty()) {
Log.w(TAG, "GoTo ignored: empty location") Log.w(TAG, "GoTo ignored: empty location")
return return
} }
val ok = navController.goTo(target) val ok = navController.goTo(target, backwards)
Log.i(TAG, "GoTo command sent: $target, result=$ok") Log.i(TAG, "GoTo command sent: $target, backwards=$backwards, result=$ok")
} }
private fun speak(text: String, langCode: String?) { private fun speak(text: String, langCode: String?) {
val content = text.trim() val content = text.trim()
if (content.isEmpty()) { if (content.isEmpty()) {
Log.w(TAG, "Speak ignored: empty text") // Log.w(TAG, "Speak ignored: empty text") // Too noisy for stream?
return return
} }
val language = resolveLanguage(langCode) val language = resolveLanguage(langCode)
scope.launch(Dispatchers.Main) { scope.launch(Dispatchers.Main) {
val ttsRequest = TtsRequest.create(content, false, language = language) 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) { class NavController(private val robot: Robot) {
private val TAG = "NavController" private val TAG = "NavController"
fun goTo(location: String): Boolean { fun goTo(location: String, backwards: Boolean = false): Boolean {
robot.goTo(location) robot.goTo(location, backwards)
return true return true
} }
@@ -24,6 +24,11 @@ class NavController(private val robot: Robot) {
robot.patrol(locations, nonStop, times, waiting) robot.patrol(locations, nonStop, times, waiting)
} }
fun repose(): Boolean {
robot.repose()
return true
}
fun randomPatrol() { fun randomPatrol() {
val allLocations = getAllLocations() val allLocations = getAllLocations()
if (allLocations.size < 3) { if (allLocations.size < 3) {