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) {
|
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}")
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user