feat: 添加接待模式和HTTP工作流集成

- 新增接待模式:机器人可前往指定位置,检测到人后显示确认按钮
- 添加HttpManager用于执行HTTP工作流,支持Home Base的人员检测触发
- 移除repose监控功能,改用OnReposeStatusChangedListener回调
- 简化界面布局,移除随机表情和说话按钮
- 在AndroidManifest中添加usesCleartextTraffic以允许HTTP通信
This commit is contained in:
2026-03-11 19:39:25 +08:00
parent c3a37123c6
commit 005932613e
5 changed files with 204 additions and 68 deletions

View File

@@ -15,6 +15,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Lzwcaiterminaltemi"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<meta-data
android:name="com.robotemi.sdk.metadata.SKILL"

View File

@@ -0,0 +1,71 @@
package com.example.lzwcai_terminal_temi
import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
object HttpManager {
private const val TAG = "HttpManager"
suspend fun workflow_execute(context: Context, apiKey: String, workflowId: String, inputs: Any): String? = withContext(Dispatchers.IO) {
var result: String? = null
try {
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
val ip = prefs.getString("network_ip", "") ?: ""
if (ip.isEmpty()) {
Log.e(TAG, "No IP address configured")
return@withContext null
}
val workflowUrl = "http://$ip:8088/open/workflow/execute"
val url = URL(workflowUrl)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.setRequestProperty("Content-Type", "application/json")
connection.setRequestProperty("X-API-Key", apiKey)
connection.doOutput = true
connection.doInput = true
connection.connectTimeout = 10000
connection.readTimeout = 10000
val jsonBody = JSONObject()
jsonBody.put("workflowId", workflowId)
when (inputs) {
is Map<*, *> -> jsonBody.put("inputs", JSONObject(inputs))
is List<*> -> jsonBody.put("inputs", JSONArray(inputs))
is Array<*> -> jsonBody.put("inputs", JSONArray(inputs))
else -> jsonBody.put("inputs", inputs)
}
Log.d(TAG, "Request Body: $jsonBody")
val writer = OutputStreamWriter(connection.outputStream)
writer.write(jsonBody.toString())
writer.flush()
writer.close()
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val reader = connection.inputStream.bufferedReader()
result = reader.readText()
reader.close()
Log.i(TAG, "Workflow execute success: $result")
} else {
val errorStream = connection.errorStream
val errorMsg = errorStream?.bufferedReader()?.readText() ?: "Unknown error"
Log.e(TAG, "Workflow execute failed: $responseCode - $errorMsg")
}
connection.disconnect()
} catch (e: Exception) {
Log.e(TAG, "Workflow execute exception", e)
}
return@withContext result
}
}

View File

@@ -12,14 +12,25 @@ 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.OnDetectionStateChangedListener
import com.robotemi.sdk.listeners.OnGoToLocationStatusChangedListener
import com.robotemi.sdk.listeners.OnRobotReadyListener
import com.robotemi.sdk.listeners.OnDetectionStateChangedListener.Companion.DETECTED
import com.robotemi.sdk.listeners.OnDetectionStateChangedListener.Companion.IDLE
import com.robotemi.sdk.navigation.listener.OnReposeStatusChangedListener
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.cancel
class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnGoToLocationStatusChangedListener, SharedPreferences.OnSharedPreferenceChangeListener {
class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnGoToLocationStatusChangedListener,
OnDetectionStateChangedListener, OnReposeStatusChangedListener, SharedPreferences.OnSharedPreferenceChangeListener {
private lateinit var robot: Robot
private lateinit var binding: ActivityMainBinding
private var mqttManager: MqttManager? = null
private val mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private lateinit var prefs: SharedPreferences
private lateinit var navCon: NavController
private var lastArrivalLocation: String? = null
@@ -27,6 +38,12 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
private val fixedFaceScale = 1.0f
private val baseFaceSizeDp = 1000f
// Reception mode
private var isReceptionMode = false
private var receptionLocation: String = ""
private var receptionText: String = ""
private var receptionDestination: String = ""
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -44,16 +61,16 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
startActivity(Intent(this, SettingsActivity::class.java))
}
binding.btnRandomExpression.setOnClickListener {
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.values().random()
}
binding.btnSpeak.setOnClickListener {
speakLongSentence()
}
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
applyFaceScale(fixedFaceScale)
binding.btnReception.setOnClickListener {
val destination = receptionDestination
stopReceptionMode()
val ttsRequest = TtsRequest.create("接待任务确认,请跟我来", false, language = TtsRequest.Language.ZH_CN)
robot.speak(ttsRequest)
navCon.goTo(destination, false)
}
updateMqttConnection()
}
@@ -63,6 +80,8 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
robot.addOnRobotReadyListener(this)
robot.addTtsListener(this)
robot.addOnGoToLocationStatusChangedListener(this)
robot.addOnDetectionStateChangedListener(this)
robot.addOnReposeStatusChangedListener(this)
prefs.registerOnSharedPreferenceChangeListener(this)
mqttManager?.connect()
}
@@ -72,6 +91,8 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
robot.removeOnRobotReadyListener(this)
robot.removeTtsListener(this)
robot.removeOnGoToLocationStatusChangedListener(this)
robot.removeOnDetectionStateChangedListener(this)
robot.removeOnReposeStatusChangedListener(this)
prefs.unregisterOnSharedPreferenceChangeListener(this)
mqttManager?.disconnect()
}
@@ -80,6 +101,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
super.onDestroy()
mqttManager?.disconnect()
LogManager.stopLogcatListener()
mainScope.cancel()
Log.i("MainActivity", "All resources released on destroy.")
}
@@ -89,16 +111,8 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
}
}
private fun speakLongSentence() {
val longSentence = "你好,我是一个智能机器人。我很高兴能为您服务。现在,我将为您展示我的各种表情和语音能力。希望您喜欢!"
val ttsRequest = TtsRequest.create(longSentence, false, language = TtsRequest.Language.ZH_CN)
robot.speak(ttsRequest)
}
override fun onTtsStatusChanged(ttsRequest: TtsRequest) {
// Forward TTS status to MqttManager for stream queue handling
mqttManager?.handleTtsStatusChange(ttsRequest)
when (ttsRequest.status) {
TtsRequest.Status.STARTED -> {
Log.i("MainActivity", "TTS started: ${ttsRequest.speech}")
@@ -137,6 +151,92 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
Log.i("MainActivity", "Arrived at $location, announcement sent.")
}
override fun onDetectionStateChanged(state: Int) {
// Reception mode logic
if (isReceptionMode && lastArrivalLocation == receptionLocation) {
when (state) {
DETECTED -> {
if (binding.btnReception.visibility != android.view.View.VISIBLE) {
binding.btnReception.visibility = android.view.View.VISIBLE
val ttsRequest = TtsRequest.create(receptionText, false, language = TtsRequest.Language.ZH_CN)
robot.speak(ttsRequest)
Log.i("MainActivity", "Reception: Person detected (new session) at $receptionLocation")
}
}
IDLE -> {
binding.btnReception.visibility = android.view.View.GONE
Log.i("MainActivity", "Reception: Person left (IDLE)")
}
}
}
// Home Base logic
if (lastArrivalLocation?.lowercase() == "home base") {
when (state) {
DETECTED -> {
mainScope.launch {
HttpManager.workflow_execute(
context = this@MainActivity,
apiKey = "wf_865e80f5fc1a4a319474a21d47470863",
workflowId = "2031297462423851009",
inputs = emptyMap<String, Any>()
)
}
}
IDLE -> {
mainScope.launch {
HttpManager.workflow_execute(
context = this@MainActivity,
apiKey = "wf_c02aa853371345dbb29572641d083c24",
workflowId = "2031634633458520065",
inputs = emptyMap<String, Any>()
)
}
}
}
}
}
fun startReceptionMode(location: String, text: String, destination: String) {
isReceptionMode = true
receptionLocation = location
receptionText = text
receptionDestination = destination
Log.i("MainActivity", "Reception mode started: location=$location, text=$text, dest=$destination")
if (lastArrivalLocation != location) {
navCon.goTo(location, false)
}
}
private fun stopReceptionMode() {
isReceptionMode = false
receptionLocation = ""
receptionText = ""
receptionDestination = ""
binding.btnReception.visibility = android.view.View.GONE
Log.i("MainActivity", "Reception mode stopped")
}
override fun onReposeStatusChanged(status: Int, description: String) {
when (status) {
OnReposeStatusChangedListener.REPOSING_COMPLETE -> {
val ttsRequest = TtsRequest.create("重新定位成功", false, language = TtsRequest.Language.ZH_CN)
robot.speak(ttsRequest)
Log.i("MainActivity", "Repose complete")
}
OnReposeStatusChangedListener.REPOSING_ABORT -> {
val ttsRequest = TtsRequest.create("重新定位被中断", false, language = TtsRequest.Language.ZH_CN)
robot.speak(ttsRequest)
Log.w("MainActivity", "Repose aborted")
}
OnReposeStatusChangedListener.REPOSING_START -> {
val ttsRequest = TtsRequest.create("正在进行重新定位", false, language = TtsRequest.Language.ZH_CN)
robot.speak(ttsRequest)
Log.i("MainActivity", "Repose started")
}
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == "network_ip") {
Log.i("MainActivity", "IP address changed, re-initializing MQTT connection.")

View File

@@ -97,7 +97,6 @@ class MqttManager(
}
fun disconnect() {
robot.removeTtsListener(this)
scope.launch {
try {
reconnectJob?.cancel()
@@ -178,9 +177,7 @@ class MqttManager(
}
"repose" -> {
val ok = navController.repose()
speak("正在进行重新定位")
Log.i(TAG, "Repose command sent: $ok")
monitorReposeStatus()
}
"stop" -> {
navController.stop()
@@ -189,6 +186,12 @@ class MqttManager(
"patrol" -> {
navController.randomPatrol()
}
"reception" -> {
val location = obj.optString("location", "前台")
val text = obj.optString("text", "你是我要接待的贵宾吗?")
val destination = obj.optString("destination", "会议室")
(context as? MainActivity)?.startReceptionMode(location, text, destination)
}
else -> Log.w(TAG, "Unknown command action: $action")
}
}
@@ -238,35 +241,6 @@ class MqttManager(
}
}
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()
if (target.isEmpty()) {
@@ -298,7 +272,7 @@ class MqttManager(
}
}
override fun handleTtsStatusChange(ttsRequest: TtsRequest) {
fun handleTtsStatusChange(ttsRequest: TtsRequest) {
scope.launch(Dispatchers.Main) {
when (ttsRequest.status) {
TtsRequest.Status.STARTED -> {
@@ -307,7 +281,7 @@ class MqttManager(
TtsRequest.Status.COMPLETED,
TtsRequest.Status.CANCELED,
TtsRequest.Status.ERROR,
TtsRequest.Status.ABORTED -> {
TtsRequest.Status.NOT_ALLOWED -> {
isTtsBusy = false
processNextTts()
}

View File

@@ -22,26 +22,16 @@
android:layout_height="500dp"
android:layout_centerInParent="true" />
<LinearLayout
<Button
android:id="@+id/btnReception"
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"
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>
android:layout_marginBottom="100dp"
android:text="是的"
android:textSize="24sp"
android:padding="20dp"
android:visibility="gone" />
</RelativeLayout>