feat: 添加导航控制与MQTT命令处理功能
- 新增NavController类,封装机器人导航相关操作(前往、停止、巡逻等) - 扩展MqttManager以支持JSON命令解析,处理导航与语音指令 - 在AndroidManifest中添加temimetadata声明,使应用作为技能运行 - 移除设置界面中的日志显示功能,简化UI - 优化主界面布局结构,修复缩进问题 - 添加到达目的地自动语音播报功能 - 固定表情视图尺寸,确保显示一致性
This commit is contained in:
@@ -16,6 +16,9 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Lzwcaiterminaltemi"
|
||||
tools:targetApi="31">
|
||||
<meta-data
|
||||
android:name="com.robotemi.sdk.metadata.SKILL"
|
||||
android:value="@string/app_name" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
|
||||
@@ -12,14 +12,20 @@ import com.example.lzwcai_terminal_temi.databinding.ActivityMainBinding
|
||||
import com.robotemi.sdk.Robot
|
||||
import com.robotemi.sdk.TtsRequest
|
||||
import com.robotemi.sdk.Robot.TtsListener
|
||||
import com.robotemi.sdk.listeners.OnGoToLocationStatusChangedListener
|
||||
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 binding: ActivityMainBinding
|
||||
private var mqttManager: MqttManager? = null
|
||||
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")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -30,6 +36,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, Sha
|
||||
LogManager.startLogcatListener()
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
|
||||
robot = Robot.getInstance()
|
||||
navCon = NavController(robot)
|
||||
|
||||
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
|
||||
@@ -46,6 +53,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, Sha
|
||||
}
|
||||
|
||||
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
|
||||
applyFaceScale(fixedFaceScale)
|
||||
|
||||
updateMqttConnection()
|
||||
}
|
||||
@@ -54,6 +62,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, Sha
|
||||
super.onStart()
|
||||
robot.addOnRobotReadyListener(this)
|
||||
robot.addTtsListener(this)
|
||||
robot.addOnGoToLocationStatusChangedListener(this)
|
||||
prefs.registerOnSharedPreferenceChangeListener(this)
|
||||
mqttManager?.connect()
|
||||
}
|
||||
@@ -62,13 +71,13 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, Sha
|
||||
super.onStop()
|
||||
robot.removeOnRobotReadyListener(this)
|
||||
robot.removeTtsListener(this)
|
||||
robot.removeOnGoToLocationStatusChangedListener(this)
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
||||
mqttManager?.disconnect()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// 确保在应用销毁时彻底释放资源
|
||||
mqttManager?.disconnect()
|
||||
LogManager.stopLogcatListener()
|
||||
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?) {
|
||||
if (key == "network_ip") {
|
||||
Log.i("MainActivity", "IP address changed, re-initializing MQTT connection.")
|
||||
@@ -119,7 +145,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, Sha
|
||||
mqttManager?.disconnect()
|
||||
val ip = prefs.getString("network_ip", null)
|
||||
if (!ip.isNullOrEmpty()) {
|
||||
mqttManager = MqttManager(this, ip)
|
||||
mqttManager = MqttManager(this, ip, robot, navCon)
|
||||
mqttManager?.connect()
|
||||
Log.i("MainActivity", "MQTT Manager updated with new IP: $ip")
|
||||
} else {
|
||||
@@ -127,4 +153,14 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, Sha
|
||||
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.util.Log
|
||||
import com.robotemi.sdk.Robot
|
||||
import com.robotemi.sdk.TtsRequest
|
||||
import kotlinx.coroutines.*
|
||||
import org.eclipse.paho.client.mqttv3.*
|
||||
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 val TAG = "MqttManager"
|
||||
@@ -33,10 +41,10 @@ class MqttManager(private val context: Context, private val serverIp: String) {
|
||||
override fun messageArrived(topic: String?, message: MqttMessage?) {
|
||||
val payload = String(message?.payload ?: ByteArray(0))
|
||||
Log.i(TAG, "Message arrived: Topic=$topic, Payload=$payload")
|
||||
handleIncomingMessage(topic, payload)
|
||||
}
|
||||
|
||||
override fun deliveryComplete(token: IMqttDeliveryToken?) {
|
||||
// This is not critical for our use case
|
||||
}
|
||||
})
|
||||
} catch (e: MqttException) {
|
||||
@@ -70,7 +78,7 @@ class MqttManager(private val context: Context, private val serverIp: String) {
|
||||
|
||||
private fun scheduleReconnect() {
|
||||
if (reconnectJob?.isActive == true) {
|
||||
return // 如果已经在计划重连,则不再创建新的
|
||||
return
|
||||
}
|
||||
reconnectJob = scope.launch {
|
||||
Log.d(TAG, "Scheduling reconnect in 5 seconds.")
|
||||
@@ -82,7 +90,7 @@ class MqttManager(private val context: Context, private val serverIp: String) {
|
||||
fun disconnect() {
|
||||
scope.launch {
|
||||
try {
|
||||
reconnectJob?.cancel() // 取消任何待处理的重连任务
|
||||
reconnectJob?.cancel()
|
||||
if (mqttClient?.isConnected == true) {
|
||||
mqttClient?.disconnect()
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -6,42 +6,42 @@
|
||||
android:padding="16dp"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSettings"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/btn_settings"
|
||||
android:src="@android:drawable/ic_menu_manage" />
|
||||
<ImageButton
|
||||
android:id="@+id/btnSettings"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/btn_settings"
|
||||
android:src="@android:drawable/ic_menu_manage" />
|
||||
|
||||
<com.example.lzwcai_terminal_temi.AnimatedEmojiView
|
||||
android:id="@+id/animatedEmojiView"
|
||||
android:layout_width="500dp"
|
||||
android:layout_height="500dp"
|
||||
android:layout_centerInParent="true" />
|
||||
<com.example.lzwcai_terminal_temi.AnimatedEmojiView
|
||||
android:id="@+id/animatedEmojiView"
|
||||
android:layout_width="500dp"
|
||||
android:layout_height="500dp"
|
||||
android:layout_centerInParent="true" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnRandomExpression"
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/btn_random_expression" />
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSpeak"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/btn_speak" />
|
||||
</LinearLayout>
|
||||
<Button
|
||||
android:id="@+id/btnRandomExpression"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/btn_random_expression" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSpeak"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/btn_speak" />
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -91,38 +91,5 @@
|
||||
</FrameLayout>
|
||||
</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>
|
||||
</ScrollView>
|
||||
@@ -8,12 +8,8 @@
|
||||
<string name="hint_ip_address">请输入 IP 地址 (例如 192.168.1.100)</string>
|
||||
<string name="btn_save">保存</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_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_speak">让机器人说话</string>
|
||||
<string name="btn_restart_app">长按重启应用</string>
|
||||
|
||||
Reference in New Issue
Block a user