feat: 添加导航控制与MQTT命令处理功能
- 新增NavController类,封装机器人导航相关操作(前往、停止、巡逻等) - 扩展MqttManager以支持JSON命令解析,处理导航与语音指令 - 在AndroidManifest中添加temimetadata声明,使应用作为技能运行 - 移除设置界面中的日志显示功能,简化UI - 优化主界面布局结构,修复缩进问题 - 添加到达目的地自动语音播报功能 - 固定表情视图尺寸,确保显示一致性
This commit is contained in:
@@ -16,6 +16,9 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Lzwcaiterminaltemi"
|
android:theme="@style/Theme.Lzwcaiterminaltemi"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
<meta-data
|
||||||
|
android:name="com.robotemi.sdk.metadata.SKILL"
|
||||||
|
android:value="@string/app_name" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|||||||
@@ -12,14 +12,20 @@ import com.example.lzwcai_terminal_temi.databinding.ActivityMainBinding
|
|||||||
import com.robotemi.sdk.Robot
|
import com.robotemi.sdk.Robot
|
||||||
import com.robotemi.sdk.TtsRequest
|
import com.robotemi.sdk.TtsRequest
|
||||||
import com.robotemi.sdk.Robot.TtsListener
|
import com.robotemi.sdk.Robot.TtsListener
|
||||||
|
import com.robotemi.sdk.listeners.OnGoToLocationStatusChangedListener
|
||||||
import com.robotemi.sdk.listeners.OnRobotReadyListener
|
import com.robotemi.sdk.listeners.OnRobotReadyListener
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnGoToLocationStatusChangedListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
private lateinit var robot: Robot
|
private lateinit var robot: Robot
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
private var mqttManager: MqttManager? = null
|
private var mqttManager: MqttManager? = null
|
||||||
private lateinit var prefs: SharedPreferences
|
private lateinit var prefs: SharedPreferences
|
||||||
|
private lateinit var navCon: NavController
|
||||||
|
private var lastArrivalLocation: String? = null
|
||||||
|
private var lastArrivalAt: Long = 0L
|
||||||
|
private val fixedFaceScale = 1.0f
|
||||||
|
private val baseFaceSizeDp = 1000f
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -30,6 +36,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, Sha
|
|||||||
LogManager.startLogcatListener()
|
LogManager.startLogcatListener()
|
||||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
|
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
|
||||||
robot = Robot.getInstance()
|
robot = Robot.getInstance()
|
||||||
|
navCon = NavController(robot)
|
||||||
|
|
||||||
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
@@ -46,6 +53,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, Sha
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
|
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
|
||||||
|
applyFaceScale(fixedFaceScale)
|
||||||
|
|
||||||
updateMqttConnection()
|
updateMqttConnection()
|
||||||
}
|
}
|
||||||
@@ -54,6 +62,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, Sha
|
|||||||
super.onStart()
|
super.onStart()
|
||||||
robot.addOnRobotReadyListener(this)
|
robot.addOnRobotReadyListener(this)
|
||||||
robot.addTtsListener(this)
|
robot.addTtsListener(this)
|
||||||
|
robot.addOnGoToLocationStatusChangedListener(this)
|
||||||
prefs.registerOnSharedPreferenceChangeListener(this)
|
prefs.registerOnSharedPreferenceChangeListener(this)
|
||||||
mqttManager?.connect()
|
mqttManager?.connect()
|
||||||
}
|
}
|
||||||
@@ -62,13 +71,13 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, Sha
|
|||||||
super.onStop()
|
super.onStop()
|
||||||
robot.removeOnRobotReadyListener(this)
|
robot.removeOnRobotReadyListener(this)
|
||||||
robot.removeTtsListener(this)
|
robot.removeTtsListener(this)
|
||||||
|
robot.removeOnGoToLocationStatusChangedListener(this)
|
||||||
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
||||||
mqttManager?.disconnect()
|
mqttManager?.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
// 确保在应用销毁时彻底释放资源
|
|
||||||
mqttManager?.disconnect()
|
mqttManager?.disconnect()
|
||||||
LogManager.stopLogcatListener()
|
LogManager.stopLogcatListener()
|
||||||
Log.i("MainActivity", "All resources released on destroy.")
|
Log.i("MainActivity", "All resources released on destroy.")
|
||||||
@@ -108,6 +117,23 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, Sha
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onGoToLocationStatusChanged(location: String, status: String, descriptionId: Int, description: String) {
|
||||||
|
val normalized = status.lowercase()
|
||||||
|
if (normalized != "complete") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (lastArrivalLocation == location && now - lastArrivalAt < 5000L) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastArrivalLocation = location
|
||||||
|
lastArrivalAt = now
|
||||||
|
val text = "已到达$location"
|
||||||
|
val ttsRequest = TtsRequest.create(text, false, language = TtsRequest.Language.ZH_CN)
|
||||||
|
robot.speak(ttsRequest)
|
||||||
|
Log.i("MainActivity", "Arrived at $location, announcement sent.")
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
if (key == "network_ip") {
|
if (key == "network_ip") {
|
||||||
Log.i("MainActivity", "IP address changed, re-initializing MQTT connection.")
|
Log.i("MainActivity", "IP address changed, re-initializing MQTT connection.")
|
||||||
@@ -119,7 +145,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, Sha
|
|||||||
mqttManager?.disconnect()
|
mqttManager?.disconnect()
|
||||||
val ip = prefs.getString("network_ip", null)
|
val ip = prefs.getString("network_ip", null)
|
||||||
if (!ip.isNullOrEmpty()) {
|
if (!ip.isNullOrEmpty()) {
|
||||||
mqttManager = MqttManager(this, ip)
|
mqttManager = MqttManager(this, ip, robot, navCon)
|
||||||
mqttManager?.connect()
|
mqttManager?.connect()
|
||||||
Log.i("MainActivity", "MQTT Manager updated with new IP: $ip")
|
Log.i("MainActivity", "MQTT Manager updated with new IP: $ip")
|
||||||
} else {
|
} else {
|
||||||
@@ -127,4 +153,14 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, Sha
|
|||||||
Log.w("MainActivity", "MQTT Manager disabled: IP address is not set.")
|
Log.w("MainActivity", "MQTT Manager disabled: IP address is not set.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun applyFaceScale(scale: Float) {
|
||||||
|
val density = resources.displayMetrics.density
|
||||||
|
val sizePx = (baseFaceSizeDp * density * scale).toInt()
|
||||||
|
val params = binding.animatedEmojiView.layoutParams
|
||||||
|
params.width = sizePx
|
||||||
|
params.height = sizePx
|
||||||
|
binding.animatedEmojiView.layoutParams = params
|
||||||
|
binding.animatedEmojiView.requestLayout()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,19 @@ package com.example.lzwcai_terminal_temi
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.robotemi.sdk.Robot
|
||||||
|
import com.robotemi.sdk.TtsRequest
|
||||||
import kotlinx.coroutines.*
|
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
|
||||||
|
|
||||||
class MqttManager(private val context: Context, private val serverIp: String) {
|
class MqttManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val serverIp: String,
|
||||||
|
private val robot: Robot,
|
||||||
|
private val navController: NavController
|
||||||
|
) {
|
||||||
|
|
||||||
private var mqttClient: MqttClient? = null
|
private var mqttClient: MqttClient? = null
|
||||||
private val TAG = "MqttManager"
|
private val TAG = "MqttManager"
|
||||||
@@ -33,10 +41,10 @@ class MqttManager(private val context: Context, private val serverIp: String) {
|
|||||||
override fun messageArrived(topic: String?, message: MqttMessage?) {
|
override fun messageArrived(topic: String?, message: MqttMessage?) {
|
||||||
val payload = String(message?.payload ?: ByteArray(0))
|
val payload = String(message?.payload ?: ByteArray(0))
|
||||||
Log.i(TAG, "Message arrived: Topic=$topic, Payload=$payload")
|
Log.i(TAG, "Message arrived: Topic=$topic, Payload=$payload")
|
||||||
|
handleIncomingMessage(topic, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deliveryComplete(token: IMqttDeliveryToken?) {
|
override fun deliveryComplete(token: IMqttDeliveryToken?) {
|
||||||
// This is not critical for our use case
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (e: MqttException) {
|
} catch (e: MqttException) {
|
||||||
@@ -70,7 +78,7 @@ class MqttManager(private val context: Context, private val serverIp: String) {
|
|||||||
|
|
||||||
private fun scheduleReconnect() {
|
private fun scheduleReconnect() {
|
||||||
if (reconnectJob?.isActive == true) {
|
if (reconnectJob?.isActive == true) {
|
||||||
return // 如果已经在计划重连,则不再创建新的
|
return
|
||||||
}
|
}
|
||||||
reconnectJob = scope.launch {
|
reconnectJob = scope.launch {
|
||||||
Log.d(TAG, "Scheduling reconnect in 5 seconds.")
|
Log.d(TAG, "Scheduling reconnect in 5 seconds.")
|
||||||
@@ -82,7 +90,7 @@ class MqttManager(private val context: Context, private val serverIp: String) {
|
|||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
reconnectJob?.cancel() // 取消任何待处理的重连任务
|
reconnectJob?.cancel()
|
||||||
if (mqttClient?.isConnected == true) {
|
if (mqttClient?.isConnected == true) {
|
||||||
mqttClient?.disconnect()
|
mqttClient?.disconnect()
|
||||||
Log.i(TAG, "Disconnected from MQTT broker.")
|
Log.i(TAG, "Disconnected from MQTT broker.")
|
||||||
@@ -123,4 +131,76 @@ class MqttManager(private val context: Context, private val serverIp: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleIncomingMessage(topic: String?, payload: String) {
|
||||||
|
val trimmed = payload.trim()
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!trimmed.startsWith("{")) {
|
||||||
|
Log.w(TAG, "Ignored non-JSON payload on ${topic ?: "unknown"}: $payload")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val obj = JSONObject(trimmed)
|
||||||
|
handleJsonCommand(obj)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Invalid JSON payload: $payload")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleJsonCommand(obj: JSONObject) {
|
||||||
|
val action = obj.optString("action", obj.optString("cmd", obj.optString("type", ""))).lowercase()
|
||||||
|
when (action) {
|
||||||
|
"goto" -> {
|
||||||
|
val location = obj.optString("location", obj.optString("target", ""))
|
||||||
|
goTo(location)
|
||||||
|
}
|
||||||
|
"speak" -> {
|
||||||
|
val text = obj.optString("text", obj.optString("speech", ""))
|
||||||
|
val lang = obj.optString("lang", "")
|
||||||
|
speak(text, lang)
|
||||||
|
}
|
||||||
|
"stop" -> {
|
||||||
|
navController.stop()
|
||||||
|
}
|
||||||
|
"patrol" -> {
|
||||||
|
navController.randomPatrol()
|
||||||
|
}
|
||||||
|
else -> Log.w(TAG, "Unknown command action: $action")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun goTo(location: String) {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun speak(text: String, langCode: String?) {
|
||||||
|
val content = text.trim()
|
||||||
|
if (content.isEmpty()) {
|
||||||
|
Log.w(TAG, "Speak ignored: empty text")
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveLanguage(langCode: String?): TtsRequest.Language {
|
||||||
|
val code = langCode?.trim()?.lowercase().orEmpty()
|
||||||
|
return when (code) {
|
||||||
|
"zh", "zh_cn", "zh-cn" -> TtsRequest.Language.ZH_CN
|
||||||
|
"en", "en_us", "en-us" -> TtsRequest.Language.EN_US
|
||||||
|
else -> TtsRequest.Language.ZH_CN
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.example.lzwcai_terminal_temi
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.robotemi.sdk.Robot
|
||||||
|
|
||||||
|
class NavController(private val robot: Robot) {
|
||||||
|
private val TAG = "NavController"
|
||||||
|
|
||||||
|
fun goTo(location: String): Boolean {
|
||||||
|
robot.goTo(location)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop(): Boolean {
|
||||||
|
robot.stopMovement()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllLocations(): List<String> {
|
||||||
|
return robot.locations
|
||||||
|
}
|
||||||
|
|
||||||
|
fun patrol(locations: List<String>, nonStop: Boolean = false, times: Int = 1, waiting: Int = 3) {
|
||||||
|
robot.patrol(locations, nonStop, times, waiting)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun randomPatrol() {
|
||||||
|
val allLocations = getAllLocations()
|
||||||
|
if (allLocations.size < 3) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val patrolCount = (3..minOf(6, allLocations.size)).random()
|
||||||
|
val patrolLocations = allLocations.shuffled().take(patrolCount)
|
||||||
|
|
||||||
|
patrol(patrolLocations, false, 1, 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,38 +91,5 @@
|
|||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@id/restartLayout"
|
|
||||||
android:layout_marginTop="32dp"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:layout_marginBottom="24dp"
|
|
||||||
android:background="#CCCCCC" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnToggleLogs"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="80dp"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:text="@string/btn_show_logs"
|
|
||||||
android:textSize="24sp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvLog"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="#EEEEEE"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:scrollbars="vertical"
|
|
||||||
android:text="@string/log_placeholder"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:visibility="gone" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -8,12 +8,8 @@
|
|||||||
<string name="hint_ip_address">请输入 IP 地址 (例如 192.168.1.100)</string>
|
<string name="hint_ip_address">请输入 IP 地址 (例如 192.168.1.100)</string>
|
||||||
<string name="btn_save">保存</string>
|
<string name="btn_save">保存</string>
|
||||||
<string name="btn_back">返回主界面</string>
|
<string name="btn_back">返回主界面</string>
|
||||||
<string name="btn_show_logs">显示日志</string>
|
|
||||||
<string name="btn_hide_logs">隐藏日志</string>
|
|
||||||
<string name="msg_ip_saved">IP 已保存: %1$s</string>
|
<string name="msg_ip_saved">IP 已保存: %1$s</string>
|
||||||
<string name="msg_invalid_ip">请输入有效的 IP 地址</string>
|
<string name="msg_invalid_ip">请输入有效的 IP 地址</string>
|
||||||
<string name="log_init">应用启动完成。目标 IP: %1$s</string>
|
|
||||||
<string name="log_placeholder">日志将显示在这里...</string>
|
|
||||||
<string name="btn_random_expression">随机表情</string>
|
<string name="btn_random_expression">随机表情</string>
|
||||||
<string name="btn_speak">让机器人说话</string>
|
<string name="btn_speak">让机器人说话</string>
|
||||||
<string name="btn_restart_app">长按重启应用</string>
|
<string name="btn_restart_app">长按重启应用</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user