feat: 添加权限管理并增强导航功能

- 新增 PermissionManager 用于检查和请求 Temi 机器人权限
- 在 AndroidManifest 中声明所需的权限元数据
- 为 NavController 添加充电功能和改进的巡逻逻辑
- 扩展 MQTT 命令支持,包括充电和可配置巡逻
- 添加 Python 测试脚本用于 MQTT 流式文本测试
- 使用任务状态跟踪替代原有的接待模式标志
This commit is contained in:
2026-03-12 16:37:59 +08:00
parent 005932613e
commit 3def989a8b
6 changed files with 247 additions and 43 deletions

View File

@@ -20,6 +20,15 @@
<meta-data <meta-data
android:name="com.robotemi.sdk.metadata.SKILL" android:name="com.robotemi.sdk.metadata.SKILL"
android:value="@string/app_name" /> android:value="@string/app_name" />
<meta-data
android:name="@string/metadata_permissions"
android:value="com.robotemi.permission.settings,
com.robotemi.permission.face_recognition,
com.robotemi.permission.map,
com.robotemi.permission.sequence,
com.robotemi.permission.meetings" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"> android:exported="true">
@@ -31,6 +40,8 @@
</activity> </activity>
<activity android:name=".SettingsActivity" /> <activity android:name=".SettingsActivity" />
<service android:name="org.eclipse.paho.android.service.MqttService" /> <service android:name="org.eclipse.paho.android.service.MqttService" />
</application> </application>

View File

@@ -18,6 +18,7 @@ import com.robotemi.sdk.listeners.OnRobotReadyListener
import com.robotemi.sdk.listeners.OnDetectionStateChangedListener.Companion.DETECTED import com.robotemi.sdk.listeners.OnDetectionStateChangedListener.Companion.DETECTED
import com.robotemi.sdk.listeners.OnDetectionStateChangedListener.Companion.IDLE import com.robotemi.sdk.listeners.OnDetectionStateChangedListener.Companion.IDLE
import com.robotemi.sdk.navigation.listener.OnReposeStatusChangedListener import com.robotemi.sdk.navigation.listener.OnReposeStatusChangedListener
import com.robotemi.sdk.permission.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -25,7 +26,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnGoToLocationStatusChangedListener, class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnGoToLocationStatusChangedListener,
OnDetectionStateChangedListener, OnReposeStatusChangedListener, SharedPreferences.OnSharedPreferenceChangeListener { OnDetectionStateChangedListener, OnReposeStatusChangedListener, SharedPreferences.OnSharedPreferenceChangeListener,
OnRequestPermissionResultListener {
private lateinit var robot: Robot private lateinit var robot: Robot
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
@@ -33,13 +35,14 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
private val mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) private val mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private lateinit var prefs: SharedPreferences private lateinit var prefs: SharedPreferences
private lateinit var navCon: NavController private lateinit var navCon: NavController
private lateinit var permissionManager: PermissionManager
private var lastArrivalLocation: String? = null private var lastArrivalLocation: String? = null
private var lastArrivalAt: Long = 0L private var lastArrivalAt: Long = 0L
private val fixedFaceScale = 1.0f private val fixedFaceScale = 1.0f
private val baseFaceSizeDp = 1000f private val baseFaceSizeDp = 1000f
private var currentTask: String = ""
// Reception mode
private var isReceptionMode = false
private var receptionLocation: String = "" private var receptionLocation: String = ""
private var receptionText: String = "" private var receptionText: String = ""
private var receptionDestination: String = "" private var receptionDestination: String = ""
@@ -54,6 +57,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
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) navCon = NavController(robot)
permissionManager = PermissionManager(robot)
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE) prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
@@ -82,6 +86,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
robot.addOnGoToLocationStatusChangedListener(this) robot.addOnGoToLocationStatusChangedListener(this)
robot.addOnDetectionStateChangedListener(this) robot.addOnDetectionStateChangedListener(this)
robot.addOnReposeStatusChangedListener(this) robot.addOnReposeStatusChangedListener(this)
robot.addOnRequestPermissionResultListener(this)
prefs.registerOnSharedPreferenceChangeListener(this) prefs.registerOnSharedPreferenceChangeListener(this)
mqttManager?.connect() mqttManager?.connect()
} }
@@ -93,6 +98,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
robot.removeOnGoToLocationStatusChangedListener(this) robot.removeOnGoToLocationStatusChangedListener(this)
robot.removeOnDetectionStateChangedListener(this) robot.removeOnDetectionStateChangedListener(this)
robot.removeOnReposeStatusChangedListener(this) robot.removeOnReposeStatusChangedListener(this)
robot.removeOnRequestPermissionResultListener(this)
prefs.unregisterOnSharedPreferenceChangeListener(this) prefs.unregisterOnSharedPreferenceChangeListener(this)
mqttManager?.disconnect() mqttManager?.disconnect()
} }
@@ -108,6 +114,21 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
override fun onRobotReady(isReady: Boolean) { override fun onRobotReady(isReady: Boolean) {
if (isReady) { if (isReady) {
Log.i("MainActivity", "Robot is ready!") Log.i("MainActivity", "Robot is ready!")
permissionManager.checkAndRequestPermissions()
}
}
override fun onRequestPermissionResult(
permission: Permission,
grantResult: Int,
requestCode: Int
) {
if (requestCode == PermissionManager.REQUEST_CODE_TEMI_PERMISSIONS) {
if (grantResult == Permission.GRANTED) {
Log.i("MainActivity", "Temi permission GRANTED: $permission")
} else {
Log.w("MainActivity", "Temi permission DENIED: $permission")
}
} }
} }
@@ -115,7 +136,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
mqttManager?.handleTtsStatusChange(ttsRequest) 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")
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.TALKING binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.TALKING
} }
TtsRequest.Status.COMPLETED -> { TtsRequest.Status.COMPLETED -> {
@@ -152,8 +173,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
} }
override fun onDetectionStateChanged(state: Int) { override fun onDetectionStateChanged(state: Int) {
// Reception mode logic if (currentTask == "reception" && lastArrivalLocation == receptionLocation) {
if (isReceptionMode && lastArrivalLocation == receptionLocation) {
when (state) { when (state) {
DETECTED -> { DETECTED -> {
if (binding.btnReception.visibility != android.view.View.VISIBLE) { if (binding.btnReception.visibility != android.view.View.VISIBLE) {
@@ -171,34 +191,34 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
} }
// Home Base logic // Home Base logic
if (lastArrivalLocation?.lowercase() == "home base") { // if (lastArrivalLocation?.lowercase() == "home base") {
when (state) { // when (state) {
DETECTED -> { // DETECTED -> {
mainScope.launch { // mainScope.launch {
HttpManager.workflow_execute( // HttpManager.workflow_execute(
context = this@MainActivity, // context = this@MainActivity,
apiKey = "wf_865e80f5fc1a4a319474a21d47470863", // apiKey = "wf_865e80f5fc1a4a319474a21d47470863",
workflowId = "2031297462423851009", // workflowId = "2031297462423851009",
inputs = emptyMap<String, Any>() // inputs = emptyMap<String, Any>()
) // )
} // }
} // }
IDLE -> { // IDLE -> {
mainScope.launch { // mainScope.launch {
HttpManager.workflow_execute( // HttpManager.workflow_execute(
context = this@MainActivity, // context = this@MainActivity,
apiKey = "wf_c02aa853371345dbb29572641d083c24", // apiKey = "wf_c02aa853371345dbb29572641d083c24",
workflowId = "2031634633458520065", // workflowId = "2031634633458520065",
inputs = emptyMap<String, Any>() // inputs = emptyMap<String, Any>()
) // )
} // }
} // }
} // }
} // }
} }
fun startReceptionMode(location: String, text: String, destination: String) { fun startReceptionMode(location: String, text: String, destination: String) {
isReceptionMode = true currentTask = "reception"
receptionLocation = location receptionLocation = location
receptionText = text receptionText = text
receptionDestination = destination receptionDestination = destination
@@ -209,7 +229,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
} }
private fun stopReceptionMode() { private fun stopReceptionMode() {
isReceptionMode = false currentTask = ""
receptionLocation = "" receptionLocation = ""
receptionText = "" receptionText = ""
receptionDestination = "" receptionDestination = ""
@@ -217,6 +237,11 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
Log.i("MainActivity", "Reception mode stopped") Log.i("MainActivity", "Reception mode stopped")
} }
fun setCurrentTask(task: String) {
currentTask = task
Log.i("MainActivity", "Current task set to: $task")
}
override fun onReposeStatusChanged(status: Int, description: String) { override fun onReposeStatusChanged(status: Int, description: String) {
when (status) { when (status) {
OnReposeStatusChangedListener.REPOSING_COMPLETE -> { OnReposeStatusChangedListener.REPOSING_COMPLETE -> {
@@ -266,4 +291,5 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
binding.animatedEmojiView.layoutParams = params binding.animatedEmojiView.layoutParams = params
binding.animatedEmojiView.requestLayout() binding.animatedEmojiView.requestLayout()
} }
} }

View File

@@ -161,6 +161,10 @@ class MqttManager(
private fun handleJsonCommand(obj: JSONObject) { private fun handleJsonCommand(obj: JSONObject) {
val action = obj.optString("action", obj.optString("cmd", obj.optString("type", ""))).lowercase() val action = obj.optString("action", obj.optString("cmd", obj.optString("type", ""))).lowercase()
when (action) { when (action) {
"charge" -> {
speak("前往充电桩", "zh")
navController.charge()
}
"goto" -> { "goto" -> {
val location = obj.optString("location", obj.optString("target", "")) val location = obj.optString("location", obj.optString("target", ""))
goTo(location) goTo(location)
@@ -184,7 +188,25 @@ class MqttManager(
stopTts() stopTts()
} }
"patrol" -> { "patrol" -> {
(context as? MainActivity)?.setCurrentTask("patrol")
speak("接到巡逻任务", "zh")
val flag = obj.optBoolean("flag", true)
if (flag) {
Log.d(TAG, "navController.randomPatrol() called.")
navController.randomPatrol() navController.randomPatrol()
} else {
val locationsArray = obj.optJSONArray("locations")
if (locationsArray != null && locationsArray.length() > 0) {
val locations = List(locationsArray.length()) {
locationsArray.getString(it)
}
Log.d(TAG, "navController.NavPatrol() called with locations: $locations")
navController.NavPatrol(locations)
} else {
Log.w(TAG, "Patrol command received without locations, falling back to random patrol.")
navController.randomPatrol()
}
}
} }
"reception" -> { "reception" -> {
val location = obj.optString("location", "前台") val location = obj.optString("location", "前台")
@@ -230,9 +252,7 @@ class MqttManager(
} }
private fun stopTts() { private fun stopTts() {
// Clear buffer
speechBuffer.setLength(0) speechBuffer.setLength(0)
scope.launch(Dispatchers.Main) { scope.launch(Dispatchers.Main) {
ttsQueue.clear() ttsQueue.clear()
isTtsBusy = false isTtsBusy = false
@@ -254,7 +274,6 @@ class MqttManager(
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") // Too noisy for stream?
return return
} }
val language = resolveLanguage(langCode) val language = resolveLanguage(langCode)

View File

@@ -5,6 +5,13 @@ 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"
private var playmode = false
fun charge(): Boolean {
playmode = !playmode
robot.goTo("home base", playmode)
return true
}
fun goTo(location: String, backwards: Boolean = false): Boolean { fun goTo(location: String, backwards: Boolean = false): Boolean {
robot.goTo(location, backwards) robot.goTo(location, backwards)
@@ -20,8 +27,12 @@ class NavController(private val robot: Robot) {
return robot.locations return robot.locations
} }
fun patrol(locations: List<String>, nonStop: Boolean = false, times: Int = 1, waiting: Int = 3) { fun NavPatrol(locations: List<String>, nonStop: Boolean = false, times: Int = 1, waiting: Int = 3) {
try {
robot.patrol(locations, nonStop, times, waiting) robot.patrol(locations, nonStop, times, waiting)
} catch (e: Exception) {
Log.e(TAG, "patrol() command failed.", e)
}
} }
fun repose(): Boolean { fun repose(): Boolean {
@@ -31,12 +42,14 @@ class NavController(private val robot: Robot) {
fun randomPatrol() { fun randomPatrol() {
val allLocations = getAllLocations() val allLocations = getAllLocations()
if (allLocations.size < 3) { val availablePatrolLocations = allLocations.filter { !it.equals("home base", ignoreCase = true) }
if (availablePatrolLocations.size < 3) {
Log.w(TAG, "Patrol command ignored: Not enough valid locations (excluding home base). Need at least 3, but found ${availablePatrolLocations.size}.")
return return
} }
val patrolCount = (3..minOf(6, allLocations.size)).random() val patrolCount = (3..minOf(6, availablePatrolLocations.size)).random()
val patrolLocations = allLocations.shuffled().take(patrolCount) val patrolLocations = availablePatrolLocations.shuffled().take(patrolCount)
Log.i(TAG, "Starting random patrol with $patrolCount locations: ${patrolLocations.joinToString()}.")
patrol(patrolLocations, false, 1, 3) NavPatrol(patrolLocations, false, 1, 5)
} }
} }

View File

@@ -0,0 +1,35 @@
package com.example.lzwcai_terminal_temi
import android.util.Log
import com.robotemi.sdk.Robot
import com.robotemi.sdk.permission.Permission
class PermissionManager(private val robot: Robot) {
private val TAG = "PermissionManager"
companion object {
const val REQUEST_CODE_TEMI_PERMISSIONS = 101
}
private val requiredPermissions = listOf(
Permission.MAP,
Permission.SEQUENCE,
Permission.FACE_RECOGNITION,
Permission.SETTINGS
)
fun checkAndRequestPermissions() {
val neededPermissions = requiredPermissions.filter {
robot.checkSelfPermission(it) != Permission.GRANTED
}
if (neededPermissions.isNotEmpty()) {
Log.i(TAG, "Requesting Temi permissions: $neededPermissions")
robot.requestPermissions(neededPermissions, REQUEST_CODE_TEMI_PERMISSIONS)
} else {
Log.i(TAG, "All required Temi permissions are already granted.")
}
}
}

100
test.py Normal file
View File

@@ -0,0 +1,100 @@
import paho.mqtt.client as mqtt
import json
import time
import sys
import random
# --- MQTT 配置 ---
# 重要:请将这里替换为您的 MQTT 代理的 IP 地址
BROKER_IP = "192.168.2.236"
BROKER_PORT = 1883
MQTT_TOPIC = "robot/cmd"
MQTT_USERNAME = "lzwc"
MQTT_PASSWORD = "Lzwc@4187."
# --- 要进行流式传输的文本 ---
# 您可以更改此文本
TEXT_TO_STREAM = "你好,我是一个先进的人工智能助理。我的设计目标是理解并生成自然语言,从而能够与人类进行流畅的对话。我可以回答问题、提供信息、撰写文章,甚至进行一些基础的编程任务。这个流式传输演示旨在展示我如何将长篇回复分解成小的数据块,并实时发送给客户端,从而创造出一种更具互动性和即时性的体验。希望这次演示能够清晰地展示我的能力。"
# --- 流媒体参数 ---
# 每个数据块中发送的字符长度范围
MIN_CHUNK_SIZE = 2
MAX_CHUNK_SIZE = 8
# 块之间的延迟(秒)
STREAM_DELAY = 0.1
def on_connect(client, userdata, flags, rc):
"""客户端连接到代理时的回调。"""
if rc == 0:
print("成功连接到 MQTT 代理!")
else:
print(f"连接失败, 返回码 {rc}\n")
sys.exit(1)
def on_publish(client, userdata, mid):
"""消息发布时的回调。"""
# 默认注释掉,因为可能会产生大量输出
# print(f"已发布消息mid: {mid}")
pass
def create_mqtt_client():
"""创建并配置 MQTT 客户端。"""
client = mqtt.Client()
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
client.on_connect = on_connect
client.on_publish = on_publish
return client
def stream_text(client, text_to_stream):
"""将给定的文本以数据块的形式流式传输到 MQTT 主题。"""
print(f"开始向主题 '{MQTT_TOPIC}' 流式传输文本:")
print(f"文本: {text_to_stream}")
index = 0
while index < len(text_to_stream):
# 生成一个随机的块大小
chunk_size = random.randint(MIN_CHUNK_SIZE, MAX_CHUNK_SIZE)
# 获取块,确保不会超出文本长度
chunk = text_to_stream[index:index + chunk_size]
index += len(chunk)
# 创建 JSON 负载
payload = {
"action": "stream",
"text": chunk,
"lang": "zh" # 假设是中文,如果需要可以更改
}
payload_json = json.dumps(payload, ensure_ascii=False)
# 发布消息
result = client.publish(MQTT_TOPIC, payload_json)
# 检查发布是否成功
if result.rc != mqtt.MQTT_ERR_SUCCESS:
print(f"发送消息失败: {mqtt.error_string(result.rc)}")
else:
print(f"已发送块: '{chunk}'")
# 等待一小段时间以模拟流式传输
time.sleep(STREAM_DELAY)
print("\n流式传输完成。")
def main():
"""运行 MQTT 流式脚本的主函数。"""
client = create_mqtt_client()
try:
client.connect(BROKER_IP, BROKER_PORT)
except Exception as e:
print(f"连接到代理 {BROKER_IP}:{BROKER_PORT} 时出错。请检查 IP 地址。")
print(f"错误: {e}")
sys.exit(1)
stream_text(client, TEXT_TO_STREAM)
client.disconnect()
print("已从 MQTT 代理断开。")
if __name__ == "__main__":
main()