Compare commits
10 Commits
0938a6ad2b
...
663768ce17
| Author | SHA1 | Date | |
|---|---|---|---|
| 663768ce17 | |||
| c898345919 | |||
| fb9dba913d | |||
| b5b5d0ad5b | |||
| c1e9ee8d34 | |||
| f89bce552a | |||
| 54b762abbf | |||
| d5ca5966f4 | |||
| 7d28490cec | |||
| 6a3431d741 |
@@ -0,0 +1,54 @@
|
|||||||
|
package com.example.lzwcai_terminal_temi
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class AutoRechargeScheduler(
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val navController: NavController,
|
||||||
|
private val getCurrentTask: () -> String,
|
||||||
|
private val isSpecialStateEnabled: () -> Boolean,
|
||||||
|
private val getLastArrivalLocation: () -> String?,
|
||||||
|
private val normalizeLocation: (String?) -> String
|
||||||
|
) {
|
||||||
|
private var autoRechargeJob: Job? = null
|
||||||
|
|
||||||
|
fun scheduleAfterIdleArrival() {
|
||||||
|
if (!isAutoRechargeAllowedTask()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (normalizeLocation(getLastArrivalLocation()) == "homebase") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
autoRechargeJob?.cancel()
|
||||||
|
autoRechargeJob = scope.launch {
|
||||||
|
delay(10_000L)
|
||||||
|
if (!isAutoRechargeAllowedTask()) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
if (normalizeLocation(getLastArrivalLocation()) == "homebase") {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
Log.i("MainActivity", "Auto recharge triggered after idle arrival timeout.")
|
||||||
|
navController.recharge()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel(reason: String) {
|
||||||
|
if (autoRechargeJob?.isActive == true) {
|
||||||
|
Log.i("MainActivity", "Auto recharge canceled: $reason")
|
||||||
|
}
|
||||||
|
autoRechargeJob?.cancel()
|
||||||
|
autoRechargeJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAutoRechargeAllowedTask(): Boolean {
|
||||||
|
if (isSpecialStateEnabled()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return MainTaskPolicy.isIdleTask(getCurrentTask())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import android.util.Log
|
|||||||
import com.robotemi.sdk.Robot
|
import com.robotemi.sdk.Robot
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
class ConnectionCoordinator(
|
class ConnectionService(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val prefs: SharedPreferences,
|
private val prefs: SharedPreferences,
|
||||||
private val robot: Robot,
|
private val robot: Robot,
|
||||||
@@ -64,7 +64,7 @@ class ConnectionCoordinator(
|
|||||||
if (host.isNullOrEmpty()) {
|
if (host.isNullOrEmpty()) {
|
||||||
mqttManager = null
|
mqttManager = null
|
||||||
mqttStatusListener(false)
|
mqttStatusListener(false)
|
||||||
Log.w("ConnectionCoordinator", "MQTT disabled: base_url is invalid or not set.")
|
Log.w("ConnectionService", "MQTT disabled: base_url is invalid or not set.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mqttManager = MqttManager(
|
mqttManager = MqttManager(
|
||||||
@@ -82,7 +82,7 @@ class ConnectionCoordinator(
|
|||||||
onPublishStatusSnapshot = onPublishStatusSnapshot
|
onPublishStatusSnapshot = onPublishStatusSnapshot
|
||||||
)
|
)
|
||||||
mqttManager?.connect()
|
mqttManager?.connect()
|
||||||
Log.i("ConnectionCoordinator", "MQTT updated with host=$host")
|
Log.i("ConnectionService", "MQTT updated with host=$host")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLiveKitConnection(isActivated: Boolean) {
|
fun updateLiveKitConnection(isActivated: Boolean) {
|
||||||
@@ -25,6 +25,8 @@ object HttpManager {
|
|||||||
const val PREF_KEY_OD_WF_KEY = "od_wf_key"
|
const val PREF_KEY_OD_WF_KEY = "od_wf_key"
|
||||||
const val PREF_KEY_CD_WFID = "cd_wfid"
|
const val PREF_KEY_CD_WFID = "cd_wfid"
|
||||||
const val PREF_KEY_CD_WF_KEY = "cd_wf_key"
|
const val PREF_KEY_CD_WF_KEY = "cd_wf_key"
|
||||||
|
const val PREF_KEY_VR_WFID = "vr_wfid"
|
||||||
|
const val PREF_KEY_VR_WF_KEY = "vr_wf_key"
|
||||||
|
|
||||||
fun getBaseUrl(context: Context): String {
|
fun getBaseUrl(context: Context): String {
|
||||||
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||||
@@ -98,6 +100,8 @@ object HttpManager {
|
|||||||
.put(PREF_KEY_OD_WF_KEY)
|
.put(PREF_KEY_OD_WF_KEY)
|
||||||
.put(PREF_KEY_CD_WFID)
|
.put(PREF_KEY_CD_WFID)
|
||||||
.put(PREF_KEY_CD_WF_KEY)
|
.put(PREF_KEY_CD_WF_KEY)
|
||||||
|
.put(PREF_KEY_VR_WFID)
|
||||||
|
.put(PREF_KEY_VR_WF_KEY)
|
||||||
.put(PREF_KEY_MQTT_PASSWORD)
|
.put(PREF_KEY_MQTT_PASSWORD)
|
||||||
val response = postJsonArray(context, "/system/config/getConfig", body, token)
|
val response = postJsonArray(context, "/system/config/getConfig", body, token)
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
|
|||||||
@@ -52,11 +52,20 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
OnDetectionStateChangedListener, OnReposeStatusChangedListener, SharedPreferences.OnSharedPreferenceChangeListener,
|
OnDetectionStateChangedListener, OnReposeStatusChangedListener, SharedPreferences.OnSharedPreferenceChangeListener,
|
||||||
OnRequestPermissionResultListener, OnBatteryStatusChangedListener, OnMovementStatusChangedListener,
|
OnRequestPermissionResultListener, OnBatteryStatusChangedListener, OnMovementStatusChangedListener,
|
||||||
OnCurrentPositionChangedListener {
|
OnCurrentPositionChangedListener {
|
||||||
|
companion object {
|
||||||
|
private const val STATE_KEY_CURRENT_TASK = "currentTask"
|
||||||
|
private const val STATE_KEY_RECEPTION_LOCATION = "receptionLocation"
|
||||||
|
private const val STATE_KEY_RECEPTION_TEXT = "receptionText"
|
||||||
|
private const val STATE_KEY_RECEPTION_DESTINATION = "receptionDestination"
|
||||||
|
private const val STATE_KEY_NOTIFICATION_LOCATION = "notificationLocation"
|
||||||
|
private const val STATE_KEY_NOTIFICATION_TEXT = "notificationText"
|
||||||
|
private const val STATE_KEY_LAST_ARRIVAL_LOCATION = "lastArrivalLocation"
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var robot: Robot
|
private lateinit var robot: Robot
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
private lateinit var uiState: UiState
|
private lateinit var uiState: UiState
|
||||||
private lateinit var connectionCoordinator: ConnectionCoordinator
|
private lateinit var connectionService: ConnectionService
|
||||||
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 val specialStateKey = "special_state"
|
private val specialStateKey = "special_state"
|
||||||
@@ -79,14 +88,6 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
private var lastArrivalLocation: String? = null
|
private var lastArrivalLocation: String? = null
|
||||||
private var lastArrivalAt: Long = 0L
|
private var lastArrivalAt: Long = 0L
|
||||||
|
|
||||||
private var initialTask: String = ""
|
|
||||||
private var initialReceptionLocation: String = ""
|
|
||||||
private var initialReceptionText: String = ""
|
|
||||||
private var initialReceptionDestination: String = ""
|
|
||||||
private var initialNotificationLocation: String = ""
|
|
||||||
private var initialNotificationText: String = ""
|
|
||||||
|
|
||||||
|
|
||||||
private var closeDoorJob: Job? = null
|
private var closeDoorJob: Job? = null
|
||||||
private var detectConfirmJob: Job? = null
|
private var detectConfirmJob: Job? = null
|
||||||
private var idleConfirmJob: Job? = null
|
private var idleConfirmJob: Job? = null
|
||||||
@@ -96,9 +97,11 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
private val idleConfirmDelayMs = 5000L
|
private val idleConfirmDelayMs = 5000L
|
||||||
private var blinkJob: Job? = null
|
private var blinkJob: Job? = null
|
||||||
private var networkErrorJob: Job? = null
|
private var networkErrorJob: Job? = null
|
||||||
private var autoRechargeJob: Job? = null
|
|
||||||
private var latestYaw: Float? = null
|
private var latestYaw: Float? = null
|
||||||
private var receptionAnchorYaw: Float? = null
|
private var receptionAnchorYaw: Float? = null
|
||||||
|
private var isTtsSpeaking: Boolean = false
|
||||||
|
private lateinit var autoRechargeScheduler: AutoRechargeScheduler
|
||||||
|
private lateinit var workflowService: WorkflowService
|
||||||
private lateinit var telemetryManager: TelemetryManager
|
private lateinit var telemetryManager: TelemetryManager
|
||||||
private lateinit var taskController: TaskController
|
private lateinit var taskController: TaskController
|
||||||
private val robotEventHandler = RobotEventHandler()
|
private val robotEventHandler = RobotEventHandler()
|
||||||
@@ -129,17 +132,15 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
navCon = NavController(robot)
|
navCon = NavController(robot)
|
||||||
permissionManager = PermissionManager(robot)
|
permissionManager = PermissionManager(robot)
|
||||||
|
|
||||||
|
val restoredTask = (savedInstanceState?.getString(STATE_KEY_CURRENT_TASK, "") ?: "")
|
||||||
|
.let { if (it == MainTaskPolicy.LEGACY_TASK_SPECIAL) "" else it }
|
||||||
|
val restoredReceptionLocation = savedInstanceState?.getString(STATE_KEY_RECEPTION_LOCATION, "") ?: ""
|
||||||
|
val restoredReceptionText = savedInstanceState?.getString(STATE_KEY_RECEPTION_TEXT, "") ?: ""
|
||||||
|
val restoredReceptionDestination = savedInstanceState?.getString(STATE_KEY_RECEPTION_DESTINATION, "") ?: ""
|
||||||
|
val restoredNotificationLocation = savedInstanceState?.getString(STATE_KEY_NOTIFICATION_LOCATION, "") ?: ""
|
||||||
|
val restoredNotificationText = savedInstanceState?.getString(STATE_KEY_NOTIFICATION_TEXT, "") ?: ""
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
initialTask = savedInstanceState.getString("currentTask", "") ?: ""
|
lastArrivalLocation = savedInstanceState.getString(STATE_KEY_LAST_ARRIVAL_LOCATION)
|
||||||
initialReceptionLocation = savedInstanceState.getString("receptionLocation", "") ?: ""
|
|
||||||
initialReceptionText = savedInstanceState.getString("receptionText", "") ?: ""
|
|
||||||
initialReceptionDestination = savedInstanceState.getString("receptionDestination", "") ?: ""
|
|
||||||
initialNotificationLocation = savedInstanceState.getString("notificationLocation", "") ?: ""
|
|
||||||
initialNotificationText = savedInstanceState.getString("notificationText", "") ?: ""
|
|
||||||
lastArrivalLocation = savedInstanceState.getString("lastArrivalLocation")
|
|
||||||
}
|
|
||||||
if (initialTask == "special") {
|
|
||||||
initialTask = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||||
@@ -164,7 +165,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
clearTaskLauncher.launch(Intent(this, SettingsActivity::class.java))
|
clearTaskLauncher.launch(Intent(this, SettingsActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionCoordinator = ConnectionCoordinator(
|
connectionService = ConnectionService(
|
||||||
context = this,
|
context = this,
|
||||||
prefs = prefs,
|
prefs = prefs,
|
||||||
robot = robot,
|
robot = robot,
|
||||||
@@ -218,12 +219,27 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
taskController.restoreState(
|
taskController.restoreState(
|
||||||
initialTask,
|
restoredTask,
|
||||||
initialReceptionLocation,
|
restoredReceptionLocation,
|
||||||
initialReceptionText,
|
restoredReceptionText,
|
||||||
initialReceptionDestination,
|
restoredReceptionDestination,
|
||||||
initialNotificationLocation,
|
restoredNotificationLocation,
|
||||||
initialNotificationText
|
restoredNotificationText
|
||||||
|
)
|
||||||
|
autoRechargeScheduler = AutoRechargeScheduler(
|
||||||
|
scope = mainScope,
|
||||||
|
navController = navCon,
|
||||||
|
getCurrentTask = { taskController.currentTask },
|
||||||
|
isSpecialStateEnabled = { isSpecialStateEnabled() },
|
||||||
|
getLastArrivalLocation = { lastArrivalLocation },
|
||||||
|
normalizeLocation = { value -> robotEventHandler.normalizeLocation(value) }
|
||||||
|
)
|
||||||
|
workflowService = WorkflowService(
|
||||||
|
context = this@MainActivity,
|
||||||
|
prefs = prefs,
|
||||||
|
scope = mainScope,
|
||||||
|
normalizeLocation = { value -> robotEventHandler.normalizeLocation(value) },
|
||||||
|
onWorkflowFailed = { showNetworkErrorBanner() }
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.btnReception.setOnClickListener {
|
binding.btnReception.setOnClickListener {
|
||||||
@@ -231,6 +247,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
if (destination.isNullOrBlank()) {
|
if (destination.isNullOrBlank()) {
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
|
workflowService.markReceptionReturnPending()
|
||||||
val ttsRequest = TtsRequest.create("接待任务确认,请跟我来", false, language = TtsRequest.Language.ZH_CN)
|
val ttsRequest = TtsRequest.create("接待任务确认,请跟我来", false, language = TtsRequest.Language.ZH_CN)
|
||||||
robot.speak(ttsRequest)
|
robot.speak(ttsRequest)
|
||||||
navCon.goTo(destination, false)
|
navCon.goTo(destination, false)
|
||||||
@@ -242,7 +259,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
locationProvider = { lastArrivalLocation },
|
locationProvider = { lastArrivalLocation },
|
||||||
mqttConnectedProvider = { isMqttConnected },
|
mqttConnectedProvider = { isMqttConnected },
|
||||||
liveKitConnectedProvider = { isLiveKitConnected },
|
liveKitConnectedProvider = { isLiveKitConnected },
|
||||||
publish = { topic, payload -> connectionCoordinator.publish(topic, payload) },
|
publish = { topic, payload -> connectionService.publish(topic, payload) },
|
||||||
onLowBattery = { _ ->
|
onLowBattery = { _ ->
|
||||||
val ttsRequest = TtsRequest.create("电量低,请及时充电。", false, language = TtsRequest.Language.ZH_CN)
|
val ttsRequest = TtsRequest.create("电量低,请及时充电。", false, language = TtsRequest.Language.ZH_CN)
|
||||||
robot.speak(ttsRequest)
|
robot.speak(ttsRequest)
|
||||||
@@ -268,8 +285,8 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
robot.constraintBeWith()
|
robot.constraintBeWith()
|
||||||
updateActivationBanner()
|
updateActivationBanner()
|
||||||
if (!isActivated()) {
|
if (!isActivated()) {
|
||||||
connectionCoordinator.disconnectMqtt()
|
connectionService.disconnectMqtt()
|
||||||
connectionCoordinator.disconnectLiveKit()
|
connectionService.disconnectLiveKit()
|
||||||
setMqttConnectionStatus(false)
|
setMqttConnectionStatus(false)
|
||||||
setLiveKitStatus(false)
|
setLiveKitStatus(false)
|
||||||
stopBlinking()
|
stopBlinking()
|
||||||
@@ -295,7 +312,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
robot.removeOnCurrentPositionChangedListener(this)
|
robot.removeOnCurrentPositionChangedListener(this)
|
||||||
robot.removeOnRequestPermissionResultListener(this)
|
robot.removeOnRequestPermissionResultListener(this)
|
||||||
// Keep MQTT alive in background/settings
|
// Keep MQTT alive in background/settings
|
||||||
connectionCoordinator.disconnectLiveKit()
|
connectionService.disconnectLiveKit()
|
||||||
stopBlinking()
|
stopBlinking()
|
||||||
telemetryManager.stop()
|
telemetryManager.stop()
|
||||||
}
|
}
|
||||||
@@ -303,7 +320,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
||||||
connectionCoordinator.release()
|
connectionService.release()
|
||||||
LogManager.stopLogcatListener()
|
LogManager.stopLogcatListener()
|
||||||
mainScope.cancel()
|
mainScope.cancel()
|
||||||
Log.i("MainActivity", "All resources released on destroy.")
|
Log.i("MainActivity", "All resources released on destroy.")
|
||||||
@@ -331,9 +348,10 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onTtsStatusChanged(ttsRequest: TtsRequest) {
|
override fun onTtsStatusChanged(ttsRequest: TtsRequest) {
|
||||||
connectionCoordinator.handleTtsStatusChange(ttsRequest)
|
connectionService.handleTtsStatusChange(ttsRequest)
|
||||||
when (ttsRequest.status) {
|
when (ttsRequest.status) {
|
||||||
TtsRequest.Status.STARTED -> {
|
TtsRequest.Status.STARTED -> {
|
||||||
|
isTtsSpeaking = true
|
||||||
Log.i("MainActivity", "TTS started")
|
Log.i("MainActivity", "TTS started")
|
||||||
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.TALKING
|
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.TALKING
|
||||||
liveKitManager?.setTtsMute(true)
|
liveKitManager?.setTtsMute(true)
|
||||||
@@ -341,8 +359,9 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
TtsRequest.Status.COMPLETED,
|
TtsRequest.Status.COMPLETED,
|
||||||
TtsRequest.Status.CANCELED,
|
TtsRequest.Status.CANCELED,
|
||||||
TtsRequest.Status.NOT_ALLOWED -> {
|
TtsRequest.Status.NOT_ALLOWED -> {
|
||||||
|
isTtsSpeaking = false
|
||||||
Log.i("MainActivity", "TTS finished: ${ttsRequest.speech}")
|
Log.i("MainActivity", "TTS finished: ${ttsRequest.speech}")
|
||||||
if (taskController.currentTask == "patrol") {
|
if (MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_PATROL)) {
|
||||||
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.ANGRY
|
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.ANGRY
|
||||||
} else {
|
} else {
|
||||||
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
|
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
|
||||||
@@ -350,6 +369,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
liveKitManager?.setTtsMute(false)
|
liveKitManager?.setTtsMute(false)
|
||||||
}
|
}
|
||||||
TtsRequest.Status.ERROR -> {
|
TtsRequest.Status.ERROR -> {
|
||||||
|
isTtsSpeaking = false
|
||||||
Log.e("MainActivity", "TTS error: ${ttsRequest.speech}")
|
Log.e("MainActivity", "TTS error: ${ttsRequest.speech}")
|
||||||
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SAD
|
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SAD
|
||||||
liveKitManager?.setTtsMute(false)
|
liveKitManager?.setTtsMute(false)
|
||||||
@@ -380,14 +400,14 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
val isAbort = robotEventHandler.isAbortStatus(status)
|
val isAbort = robotEventHandler.isAbortStatus(status)
|
||||||
val isMoving = robotEventHandler.isMovingStatus(status)
|
val isMoving = robotEventHandler.isMovingStatus(status)
|
||||||
if (isMoving) {
|
if (isMoving) {
|
||||||
cancelAutoRecharge("movement_started:$location/$status")
|
autoRechargeScheduler.cancel("movement_started:$location/$status")
|
||||||
taskController.cancelTaskWaitTimeout()
|
taskController.cancelTaskWaitTimeout()
|
||||||
}
|
}
|
||||||
if (normalized != "complete" && !isAbort) {
|
if (normalized != "complete" && !isAbort) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isAbort) {
|
if (isAbort) {
|
||||||
cancelAutoRecharge("movement_aborted:$location/$status")
|
autoRechargeScheduler.cancel("movement_aborted:$location/$status")
|
||||||
taskController.cancelTaskWaitTimeout()
|
taskController.cancelTaskWaitTimeout()
|
||||||
taskController.clearLeavingHomeBase()
|
taskController.clearLeavingHomeBase()
|
||||||
taskController.endNonSpecialTask("goTo aborted: $location, status=$status")
|
taskController.endNonSpecialTask("goTo aborted: $location, status=$status")
|
||||||
@@ -403,23 +423,26 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
prefs.edit().putString("current_location", location).apply()
|
prefs.edit().putString("current_location", location).apply()
|
||||||
if (robotEventHandler.normalizeLocation(location) == "homebase") {
|
if (robotEventHandler.normalizeLocation(location) == "homebase") {
|
||||||
navCon.tiltAngle(20)
|
navCon.tiltAngle(20)
|
||||||
|
workflowService.triggerReceptionReturnWorkflowIfNeeded(location)
|
||||||
}
|
}
|
||||||
if (taskController.currentTask == "patrol") {
|
if (MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_PATROL)) {
|
||||||
taskController.handlePatrolArrival(location)
|
taskController.handlePatrolArrival(location)
|
||||||
}
|
}
|
||||||
if (taskController.currentTask == "reception" &&
|
if (MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_RECEPTION) &&
|
||||||
location.equals(taskController.getReceptionLocation(), ignoreCase = true)
|
robotEventHandler.normalizeLocation(location) ==
|
||||||
|
robotEventHandler.normalizeLocation(taskController.getReceptionLocation())
|
||||||
) {
|
) {
|
||||||
captureReceptionAnchorYawIfNeeded()
|
captureReceptionAnchorYawIfNeeded()
|
||||||
taskController.startTaskWaitTimeout()
|
taskController.startTaskWaitTimeout()
|
||||||
}
|
}
|
||||||
if (taskController.currentTask != "reception") {
|
if (!MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_RECEPTION)) {
|
||||||
receptionAnchorYaw = null
|
receptionAnchorYaw = null
|
||||||
}
|
}
|
||||||
if (taskController.handleNotificationArrival(location)) {
|
if (taskController.handleNotificationArrival(location)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isSpecialStateEnabled() && taskController.currentTask.isEmpty()) {
|
val behavior = MainTaskPolicy.resolveBehaviorDecision(taskController.currentTask, isSpecialStateEnabled())
|
||||||
|
if (behavior.skipArrivalAnnouncement) {
|
||||||
Log.i("MainActivity", "Special state: arrival announcement skipped at $location.")
|
Log.i("MainActivity", "Special state: arrival announcement skipped at $location.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -427,7 +450,9 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
val ttsRequest = TtsRequest.create(text, false, language = TtsRequest.Language.ZH_CN)
|
val ttsRequest = TtsRequest.create(text, false, language = TtsRequest.Language.ZH_CN)
|
||||||
robot.speak(ttsRequest)
|
robot.speak(ttsRequest)
|
||||||
Log.i("MainActivity", "Arrived at $location, announcement sent.")
|
Log.i("MainActivity", "Arrived at $location, announcement sent.")
|
||||||
scheduleAutoRechargeAfterIdleArrival()
|
if (behavior.allowAutoRecharge) {
|
||||||
|
autoRechargeScheduler.scheduleAfterIdleArrival()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetectionStateChanged(state: Int) {
|
override fun onDetectionStateChanged(state: Int) {
|
||||||
@@ -467,7 +492,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
private fun handleStableDetectionStateChanged(state: Int) {
|
private fun handleStableDetectionStateChanged(state: Int) {
|
||||||
Log.i("MainActivity", "Stable detection state: $state")
|
Log.i("MainActivity", "Stable detection state: $state")
|
||||||
liveKitManager?.setDetectionActive(state == DETECTED)
|
liveKitManager?.setDetectionActive(state == DETECTED)
|
||||||
if (taskController.currentTask == "reception") {
|
if (MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_RECEPTION)) {
|
||||||
if (state == DETECTED) {
|
if (state == DETECTED) {
|
||||||
captureReceptionAnchorYawIfNeeded()
|
captureReceptionAnchorYawIfNeeded()
|
||||||
} else if (state == IDLE) {
|
} else if (state == IDLE) {
|
||||||
@@ -478,10 +503,9 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
Log.i("MainActivity", "Detection event handled by task controller.")
|
Log.i("MainActivity", "Detection event handled by task controller.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val isSpecialState = isSpecialStateEnabled()
|
val behavior = MainTaskPolicy.resolveBehaviorDecision(taskController.currentTask, isSpecialStateEnabled())
|
||||||
val isIdleTask = taskController.currentTask.isEmpty() || taskController.currentTask == "speech"
|
|
||||||
val atHomeBase = robotEventHandler.normalizeLocation(lastArrivalLocation) == "homebase"
|
val atHomeBase = robotEventHandler.normalizeLocation(lastArrivalLocation) == "homebase"
|
||||||
val canHandleDoor = isIdleTask && atHomeBase && !taskController.isLeavingHomeBase && !isSpecialState
|
val canHandleDoor = behavior.allowDoorWorkflow && atHomeBase && !taskController.isLeavingHomeBase
|
||||||
if (canHandleDoor) {
|
if (canHandleDoor) {
|
||||||
when (state) {
|
when (state) {
|
||||||
DETECTED -> {
|
DETECTED -> {
|
||||||
@@ -490,7 +514,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
val ttsRequest = TtsRequest.create("正在为你开门,请稍等。", false, language = TtsRequest.Language.ZH_CN)
|
val ttsRequest = TtsRequest.create("正在为你开门,请稍等。", false, language = TtsRequest.Language.ZH_CN)
|
||||||
robot.speak(ttsRequest)
|
robot.speak(ttsRequest)
|
||||||
mainScope.launch {
|
mainScope.launch {
|
||||||
val result = executeDoorWorkflow(openDoor = true)
|
val result = workflowService.executeDoorWorkflow(openDoor = true)
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
showNetworkErrorBanner()
|
showNetworkErrorBanner()
|
||||||
}
|
}
|
||||||
@@ -501,7 +525,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
val ttsRequest = TtsRequest.create("正准备关门,请小心被夹。", false, language = TtsRequest.Language.ZH_CN)
|
val ttsRequest = TtsRequest.create("正准备关门,请小心被夹。", false, language = TtsRequest.Language.ZH_CN)
|
||||||
robot.speak(ttsRequest)
|
robot.speak(ttsRequest)
|
||||||
mainScope.launch {
|
mainScope.launch {
|
||||||
val result = executeDoorWorkflow(openDoor = false)
|
val result = workflowService.executeDoorWorkflow(openDoor = false)
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
showNetworkErrorBanner()
|
showNetworkErrorBanner()
|
||||||
}
|
}
|
||||||
@@ -510,7 +534,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isIdleTask && !atHomeBase && state == DETECTED && !isSpecialState) {
|
if (behavior.allowIdleGreeting && !atHomeBase && state == DETECTED && !isTtsSpeaking) {
|
||||||
val hour = java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY)
|
val hour = java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY)
|
||||||
val greeting = when (hour) {
|
val greeting = when (hour) {
|
||||||
in 6..11 -> "早上好"
|
in 6..11 -> "早上好"
|
||||||
@@ -537,9 +561,9 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
|
|
||||||
fun setCurrentTask(task: String) {
|
fun setCurrentTask(task: String) {
|
||||||
if (task.trim().isNotEmpty()) {
|
if (task.trim().isNotEmpty()) {
|
||||||
cancelAutoRecharge("task_set:$task")
|
autoRechargeScheduler.cancel("task_set:$task")
|
||||||
}
|
}
|
||||||
if (!task.equals("reception", ignoreCase = true)) {
|
if (!MainTaskPolicy.isTask(task, MainTaskPolicy.TASK_RECEPTION)) {
|
||||||
receptionAnchorYaw = null
|
receptionAnchorYaw = null
|
||||||
}
|
}
|
||||||
taskController.setCurrentTask(task)
|
taskController.setCurrentTask(task)
|
||||||
@@ -574,6 +598,9 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
if (key == specialStateKey) {
|
if (key == specialStateKey) {
|
||||||
val isSpecial = isSpecialStateEnabled()
|
val isSpecial = isSpecialStateEnabled()
|
||||||
Log.i("MainActivity", "Special state pref changed: $isSpecial, currentTask: ${taskController.currentTask}")
|
Log.i("MainActivity", "Special state pref changed: $isSpecial, currentTask: ${taskController.currentTask}")
|
||||||
|
if (isSpecial) {
|
||||||
|
autoRechargeScheduler.cancel("special_state_enabled")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (key == LiveKitManager.PREF_KEY_URL ||
|
if (key == LiveKitManager.PREF_KEY_URL ||
|
||||||
key == LiveKitManager.PREF_KEY_ROOM ||
|
key == LiveKitManager.PREF_KEY_ROOM ||
|
||||||
@@ -592,11 +619,11 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMqttConnection() {
|
private fun updateMqttConnection() {
|
||||||
connectionCoordinator.updateMqttConnection(isActivated())
|
connectionService.updateMqttConnection(isActivated())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLiveKitConnection() {
|
private fun updateLiveKitConnection() {
|
||||||
connectionCoordinator.updateLiveKitConnection(isActivated())
|
connectionService.updateLiveKitConnection(isActivated())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasAudioPermission(): Boolean {
|
private fun hasAudioPermission(): Boolean {
|
||||||
@@ -679,7 +706,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLiveKitStatusSnapshot() {
|
private fun updateLiveKitStatusSnapshot() {
|
||||||
connectionCoordinator.updateLiveKitStatusSnapshot(isActivated())
|
connectionService.updateLiveKitStatusSnapshot(isActivated())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setLiveKitStatus(connected: Boolean) {
|
private fun setLiveKitStatus(connected: Boolean) {
|
||||||
@@ -706,7 +733,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
.put("topic", topicLabel)
|
.put("topic", topicLabel)
|
||||||
.put("participant", participantLabel)
|
.put("participant", participantLabel)
|
||||||
.put("ts", System.currentTimeMillis())
|
.put("ts", System.currentTimeMillis())
|
||||||
connectionCoordinator.publish("robot/asr", data.toString())
|
connectionService.publish("robot/asr", data.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractAsrText(payload: String): String? {
|
private fun extractAsrText(payload: String): String? {
|
||||||
@@ -736,35 +763,6 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scheduleAutoRechargeAfterIdleArrival() {
|
|
||||||
if (taskController.currentTask.isNotEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (robotEventHandler.normalizeLocation(lastArrivalLocation) == "homebase") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
autoRechargeJob?.cancel()
|
|
||||||
autoRechargeJob = mainScope.launch {
|
|
||||||
delay(10_000L)
|
|
||||||
if (taskController.currentTask.isNotEmpty()) {
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
if (robotEventHandler.normalizeLocation(lastArrivalLocation) == "homebase") {
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
Log.i("MainActivity", "Auto recharge triggered after idle arrival timeout.")
|
|
||||||
navCon.recharge()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancelAutoRecharge(reason: String) {
|
|
||||||
if (autoRechargeJob?.isActive == true) {
|
|
||||||
Log.i("MainActivity", "Auto recharge canceled: $reason")
|
|
||||||
}
|
|
||||||
autoRechargeJob?.cancel()
|
|
||||||
autoRechargeJob = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun captureReceptionAnchorYawIfNeeded() {
|
private fun captureReceptionAnchorYawIfNeeded() {
|
||||||
if (receptionAnchorYaw != null) {
|
if (receptionAnchorYaw != null) {
|
||||||
return
|
return
|
||||||
@@ -775,10 +773,12 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun recoverReceptionFacingDirection() {
|
private fun recoverReceptionFacingDirection() {
|
||||||
if (taskController.currentTask != "reception") {
|
if (!MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_RECEPTION)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val atReceptionLocation = lastArrivalLocation?.equals(taskController.getReceptionLocation(), ignoreCase = true) == true
|
val atReceptionLocation =
|
||||||
|
robotEventHandler.normalizeLocation(lastArrivalLocation) ==
|
||||||
|
robotEventHandler.normalizeLocation(taskController.getReceptionLocation())
|
||||||
if (!atReceptionLocation) {
|
if (!atReceptionLocation) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -797,23 +797,6 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
Log.i("MainActivity", "Reception facing recovered by $turn degrees (delta=$delta).")
|
Log.i("MainActivity", "Reception facing recovered by $turn degrees (delta=$delta).")
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun executeDoorWorkflow(openDoor: Boolean): String? {
|
|
||||||
val workflowIdKey = if (openDoor) HttpManager.PREF_KEY_OD_WFID else HttpManager.PREF_KEY_CD_WFID
|
|
||||||
val workflowApiKey = if (openDoor) HttpManager.PREF_KEY_OD_WF_KEY else HttpManager.PREF_KEY_CD_WF_KEY
|
|
||||||
val workflowId = prefs.getString(workflowIdKey, "").orEmpty().trim()
|
|
||||||
val apiKey = prefs.getString(workflowApiKey, "").orEmpty().trim()
|
|
||||||
if (workflowId.isEmpty() || apiKey.isEmpty()) {
|
|
||||||
Log.w("MainActivity", "Door workflow config missing: openDoor=$openDoor")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return HttpManager.workflow_execute(
|
|
||||||
context = this@MainActivity,
|
|
||||||
apiKey = apiKey,
|
|
||||||
workflowId = workflowId,
|
|
||||||
inputs = emptyMap<String, Any>()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isActivated(): Boolean {
|
private fun isActivated(): Boolean {
|
||||||
if (!::prefs.isInitialized) {
|
if (!::prefs.isInitialized) {
|
||||||
return false
|
return false
|
||||||
@@ -854,13 +837,13 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
outState.putString("currentTask", taskController.currentTask)
|
outState.putString(STATE_KEY_CURRENT_TASK, taskController.currentTask)
|
||||||
outState.putString("receptionLocation", taskController.getReceptionLocation())
|
outState.putString(STATE_KEY_RECEPTION_LOCATION, taskController.getReceptionLocation())
|
||||||
outState.putString("receptionText", taskController.getReceptionText())
|
outState.putString(STATE_KEY_RECEPTION_TEXT, taskController.getReceptionText())
|
||||||
outState.putString("receptionDestination", taskController.getReceptionDestination())
|
outState.putString(STATE_KEY_RECEPTION_DESTINATION, taskController.getReceptionDestination())
|
||||||
outState.putString("notificationLocation", taskController.getNotificationLocation())
|
outState.putString(STATE_KEY_NOTIFICATION_LOCATION, taskController.getNotificationLocation())
|
||||||
outState.putString("notificationText", taskController.getNotificationText())
|
outState.putString(STATE_KEY_NOTIFICATION_TEXT, taskController.getNotificationText())
|
||||||
outState.putString("lastArrivalLocation", lastArrivalLocation)
|
outState.putString(STATE_KEY_LAST_ARRIVAL_LOCATION, lastArrivalLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReposeStatusChanged(status: Int, description: String) {
|
override fun onReposeStatusChanged(status: Int, description: String) {
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.example.lzwcai_terminal_temi
|
||||||
|
|
||||||
|
object MainTaskPolicy {
|
||||||
|
const val TASK_PATROL = "patrol"
|
||||||
|
const val TASK_RECEPTION = "reception"
|
||||||
|
const val TASK_SPEECH = "speech"
|
||||||
|
const val LEGACY_TASK_SPECIAL = "special"
|
||||||
|
|
||||||
|
data class BehaviorDecision(
|
||||||
|
val skipArrivalAnnouncement: Boolean,
|
||||||
|
val allowAutoRecharge: Boolean,
|
||||||
|
val allowDoorWorkflow: Boolean,
|
||||||
|
val allowIdleGreeting: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
fun normalizeTask(task: String?): String {
|
||||||
|
return task.orEmpty().trim().lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTask(task: String?, expected: String): Boolean {
|
||||||
|
return normalizeTask(task) == expected
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isIdleTask(task: String?): Boolean {
|
||||||
|
val normalized = normalizeTask(task)
|
||||||
|
return normalized.isEmpty() || normalized == TASK_SPEECH
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resolveBehaviorDecision(task: String?, isSpecialState: Boolean): BehaviorDecision {
|
||||||
|
val isIdleTask = isIdleTask(task)
|
||||||
|
return BehaviorDecision(
|
||||||
|
skipArrivalAnnouncement = isSpecialState && normalizeTask(task).isEmpty(),
|
||||||
|
allowAutoRecharge = !isSpecialState && isIdleTask,
|
||||||
|
allowDoorWorkflow = !isSpecialState && isIdleTask,
|
||||||
|
allowIdleGreeting = !isSpecialState && isIdleTask
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,8 @@ class MqttManager(
|
|||||||
private val job = SupervisorJob()
|
private val job = SupervisorJob()
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + job)
|
private val scope = CoroutineScope(Dispatchers.IO + job)
|
||||||
private var reconnectJob: Job? = null
|
private var reconnectJob: Job? = null
|
||||||
|
@Volatile
|
||||||
|
private var isConnecting: Boolean = false
|
||||||
private val prefs = appContext.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
private val prefs = appContext.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||||
private val agentDempIdKey = "agent_demp_id"
|
private val agentDempIdKey = "agent_demp_id"
|
||||||
|
|
||||||
@@ -57,10 +59,15 @@ class MqttManager(
|
|||||||
private var danceJob: Job? = null
|
private var danceJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
createMqttClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMqttClient() {
|
||||||
try {
|
try {
|
||||||
mqttClient = MqttClient(brokerUri, clientId, MemoryPersistence())
|
mqttClient = MqttClient(brokerUri, clientId, MemoryPersistence())
|
||||||
mqttClient?.setCallback(object : MqttCallbackExtended {
|
mqttClient?.setCallback(object : MqttCallbackExtended {
|
||||||
override fun connectComplete(reconnect: Boolean, serverURI: String?) {
|
override fun connectComplete(reconnect: Boolean, serverURI: String?) {
|
||||||
|
isConnecting = false
|
||||||
Log.i(TAG, "MQTT connection complete. Reconnect: $reconnect")
|
Log.i(TAG, "MQTT connection complete. Reconnect: $reconnect")
|
||||||
subscribeTopic("robot/cmd")
|
subscribeTopic("robot/cmd")
|
||||||
subscribeTopic("soul2user")
|
subscribeTopic("soul2user")
|
||||||
@@ -68,6 +75,7 @@ class MqttManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun connectionLost(cause: Throwable?) {
|
override fun connectionLost(cause: Throwable?) {
|
||||||
|
isConnecting = false
|
||||||
Log.e(TAG, "Connection lost: ${cause?.message}")
|
Log.e(TAG, "Connection lost: ${cause?.message}")
|
||||||
updateConnectionStatus(false)
|
updateConnectionStatus(false)
|
||||||
scheduleReconnect()
|
scheduleReconnect()
|
||||||
@@ -96,6 +104,10 @@ class MqttManager(
|
|||||||
updateConnectionStatus(true)
|
updateConnectionStatus(true)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
if (isConnecting) {
|
||||||
|
Log.d(TAG, "MQTT connect skipped: already connecting.")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
val username = prefs.getString(HttpManager.PREF_KEY_MQTT_USERNAME, "").orEmpty().trim()
|
val username = prefs.getString(HttpManager.PREF_KEY_MQTT_USERNAME, "").orEmpty().trim()
|
||||||
val password = prefs.getString(HttpManager.PREF_KEY_MQTT_PASSWORD, "").orEmpty()
|
val password = prefs.getString(HttpManager.PREF_KEY_MQTT_PASSWORD, "").orEmpty()
|
||||||
if (username.isEmpty() || password.isEmpty()) {
|
if (username.isEmpty() || password.isEmpty()) {
|
||||||
@@ -103,8 +115,12 @@ class MqttManager(
|
|||||||
updateConnectionStatus(false)
|
updateConnectionStatus(false)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
if (mqttClient == null) {
|
||||||
|
createMqttClient()
|
||||||
|
}
|
||||||
Log.i(TAG, "Attempting to connect to MQTT broker at $brokerUri")
|
Log.i(TAG, "Attempting to connect to MQTT broker at $brokerUri")
|
||||||
try {
|
try {
|
||||||
|
isConnecting = true
|
||||||
val options = MqttConnectOptions().apply {
|
val options = MqttConnectOptions().apply {
|
||||||
isAutomaticReconnect = false
|
isAutomaticReconnect = false
|
||||||
isCleanSession = true
|
isCleanSession = true
|
||||||
@@ -114,8 +130,10 @@ class MqttManager(
|
|||||||
this.password = password.toCharArray()
|
this.password = password.toCharArray()
|
||||||
}
|
}
|
||||||
mqttClient?.connect(options)
|
mqttClient?.connect(options)
|
||||||
} catch (e: MqttException) {
|
} catch (t: Throwable) {
|
||||||
Log.e(TAG, "Initial connection failed: ${e.message}")
|
Log.e(TAG, "Initial connection failed", t)
|
||||||
|
isConnecting = false
|
||||||
|
resetClientSafely()
|
||||||
updateConnectionStatus(false)
|
updateConnectionStatus(false)
|
||||||
scheduleReconnect()
|
scheduleReconnect()
|
||||||
}
|
}
|
||||||
@@ -137,6 +155,7 @@ class MqttManager(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
reconnectJob?.cancel()
|
reconnectJob?.cancel()
|
||||||
|
isConnecting = false
|
||||||
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.")
|
||||||
@@ -151,7 +170,7 @@ class MqttManager(
|
|||||||
|
|
||||||
fun shutdown() {
|
fun shutdown() {
|
||||||
reconnectJob?.cancel()
|
reconnectJob?.cancel()
|
||||||
job.cancel()
|
isConnecting = false
|
||||||
try {
|
try {
|
||||||
if (mqttClient?.isConnected == true) {
|
if (mqttClient?.isConnected == true) {
|
||||||
mqttClient?.disconnect()
|
mqttClient?.disconnect()
|
||||||
@@ -165,6 +184,16 @@ class MqttManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resetClientSafely() {
|
||||||
|
try {
|
||||||
|
mqttClient?.close()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
} finally {
|
||||||
|
mqttClient = null
|
||||||
|
}
|
||||||
|
createMqttClient()
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateConnectionStatus(connected: Boolean) {
|
private fun updateConnectionStatus(connected: Boolean) {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
statusListener(connected)
|
statusListener(connected)
|
||||||
@@ -284,7 +313,8 @@ 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()
|
||||||
val actionsResetTask = setOf("recharge", "goto", "notification", "reception", "patrol", "repose", "turn", "tilt", "terminate")
|
// Only interrupt current task for explicit navigation/termination commands.
|
||||||
|
val actionsResetTask = setOf("recharge", "goto", "terminate")
|
||||||
if (action in actionsResetTask) {
|
if (action in actionsResetTask) {
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
onSetCurrentTask("")
|
onSetCurrentTask("")
|
||||||
@@ -395,9 +425,9 @@ class MqttManager(
|
|||||||
}
|
}
|
||||||
"reception" -> {
|
"reception" -> {
|
||||||
speak("接到接待任务", "zh")
|
speak("接到接待任务", "zh")
|
||||||
val location = obj.optString("location", "前台")
|
val location = "前台"
|
||||||
val text = obj.optString("text", "你是我要接待的贵宾吗?")
|
val text = obj.optString("text", "你是我要接待的贵宾吗?").trim()
|
||||||
val destination = obj.optString("destination", "会议室")
|
val destination = obj.optString("destination", "会议室").trim()
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
onStartReceptionMode(location, text, destination)
|
onStartReceptionMode(location, text, destination)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
.remove(HttpManager.PREF_KEY_OD_WF_KEY)
|
.remove(HttpManager.PREF_KEY_OD_WF_KEY)
|
||||||
.remove(HttpManager.PREF_KEY_CD_WFID)
|
.remove(HttpManager.PREF_KEY_CD_WFID)
|
||||||
.remove(HttpManager.PREF_KEY_CD_WF_KEY)
|
.remove(HttpManager.PREF_KEY_CD_WF_KEY)
|
||||||
|
.remove(HttpManager.PREF_KEY_VR_WFID)
|
||||||
|
.remove(HttpManager.PREF_KEY_VR_WF_KEY)
|
||||||
binding.etActivationCode.setText("")
|
binding.etActivationCode.setText("")
|
||||||
binding.etDeviceName.setText("")
|
binding.etDeviceName.setText("")
|
||||||
updateActivationUi(false)
|
updateActivationUi(false)
|
||||||
@@ -154,8 +156,15 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
val odWfKey = runtimeConfigs?.get(HttpManager.PREF_KEY_OD_WF_KEY).orEmpty()
|
val odWfKey = runtimeConfigs?.get(HttpManager.PREF_KEY_OD_WF_KEY).orEmpty()
|
||||||
val cdWfid = runtimeConfigs?.get(HttpManager.PREF_KEY_CD_WFID).orEmpty()
|
val cdWfid = runtimeConfigs?.get(HttpManager.PREF_KEY_CD_WFID).orEmpty()
|
||||||
val cdWfKey = runtimeConfigs?.get(HttpManager.PREF_KEY_CD_WF_KEY).orEmpty()
|
val cdWfKey = runtimeConfigs?.get(HttpManager.PREF_KEY_CD_WF_KEY).orEmpty()
|
||||||
|
val vrWfid = runtimeConfigs?.get(HttpManager.PREF_KEY_VR_WFID).orEmpty()
|
||||||
|
val vrWfKey = runtimeConfigs?.get(HttpManager.PREF_KEY_VR_WF_KEY).orEmpty()
|
||||||
val mqttReady = mqttUser.isNotBlank() && mqttPass.isNotBlank()
|
val mqttReady = mqttUser.isNotBlank() && mqttPass.isNotBlank()
|
||||||
val workflowReady = odWfid.isNotBlank() && odWfKey.isNotBlank() && cdWfid.isNotBlank() && cdWfKey.isNotBlank()
|
val workflowReady = odWfid.isNotBlank() &&
|
||||||
|
odWfKey.isNotBlank() &&
|
||||||
|
cdWfid.isNotBlank() &&
|
||||||
|
cdWfKey.isNotBlank() &&
|
||||||
|
vrWfid.isNotBlank() &&
|
||||||
|
vrWfKey.isNotBlank()
|
||||||
val configReady = mqttReady && workflowReady
|
val configReady = mqttReady && workflowReady
|
||||||
val editor = prefs.edit()
|
val editor = prefs.edit()
|
||||||
.putString(HttpManager.PREF_KEY_ACTIVATION_CODE, activationCode)
|
.putString(HttpManager.PREF_KEY_ACTIVATION_CODE, activationCode)
|
||||||
@@ -168,6 +177,8 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
.putString(HttpManager.PREF_KEY_OD_WF_KEY, odWfKey)
|
.putString(HttpManager.PREF_KEY_OD_WF_KEY, odWfKey)
|
||||||
.putString(HttpManager.PREF_KEY_CD_WFID, cdWfid)
|
.putString(HttpManager.PREF_KEY_CD_WFID, cdWfid)
|
||||||
.putString(HttpManager.PREF_KEY_CD_WF_KEY, cdWfKey)
|
.putString(HttpManager.PREF_KEY_CD_WF_KEY, cdWfKey)
|
||||||
|
.putString(HttpManager.PREF_KEY_VR_WFID, vrWfid)
|
||||||
|
.putString(HttpManager.PREF_KEY_VR_WF_KEY, vrWfKey)
|
||||||
editor.apply()
|
editor.apply()
|
||||||
updateActivationUi(true)
|
updateActivationUi(true)
|
||||||
if (!configReady) {
|
if (!configReady) {
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ class TaskController(
|
|||||||
private val setEmoji: (AnimatedEmojiView.Expression) -> Unit,
|
private val setEmoji: (AnimatedEmojiView.Expression) -> Unit,
|
||||||
private val setReceptionButtonVisible: (Boolean) -> Unit
|
private val setReceptionButtonVisible: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
|
private fun normalizeLocation(value: String?): String {
|
||||||
|
return value.orEmpty()
|
||||||
|
.trim()
|
||||||
|
.lowercase()
|
||||||
|
.replace(" ", "")
|
||||||
|
.replace("_", "")
|
||||||
|
.replace("-", "")
|
||||||
|
}
|
||||||
|
|
||||||
var currentTask: String = ""
|
var currentTask: String = ""
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -120,14 +129,21 @@ class TaskController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startReceptionMode(location: String, text: String, destination: String) {
|
fun startReceptionMode(location: String, text: String, destination: String) {
|
||||||
|
val targetLocation = location.trim()
|
||||||
|
val promptText = text.trim()
|
||||||
|
val targetDestination = destination.trim()
|
||||||
|
if (targetLocation.isEmpty()) {
|
||||||
|
setCurrentTask("")
|
||||||
|
return
|
||||||
|
}
|
||||||
setCurrentTask("reception")
|
setCurrentTask("reception")
|
||||||
receptionLocation = location
|
receptionLocation = targetLocation
|
||||||
receptionText = text
|
receptionText = promptText.ifEmpty { "你是我要接待的贵宾吗?" }
|
||||||
receptionDestination = destination
|
receptionDestination = targetDestination
|
||||||
isReceptionPromptVisible = false
|
isReceptionPromptVisible = false
|
||||||
setReceptionButtonVisible(false)
|
setReceptionButtonVisible(false)
|
||||||
if (getLastArrivalLocation() != location) {
|
if (getLastArrivalLocation()?.trim()?.equals(targetLocation, ignoreCase = true) != true) {
|
||||||
navController.goTo(location, false)
|
navController.goTo(targetLocation, false)
|
||||||
} else {
|
} else {
|
||||||
startTaskWaitTimeout()
|
startTaskWaitTimeout()
|
||||||
}
|
}
|
||||||
@@ -177,9 +193,13 @@ class TaskController(
|
|||||||
speak("别妨碍我,我正在巡逻呢")
|
speak("别妨碍我,我正在巡逻呢")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (currentTask == "reception" &&
|
if (currentTask == "reception") {
|
||||||
getLastArrivalLocation()?.equals(receptionLocation, ignoreCase = true) == true
|
val isAtReceptionLocation =
|
||||||
) {
|
normalizeLocation(getLastArrivalLocation()) == normalizeLocation(receptionLocation)
|
||||||
|
if (!isAtReceptionLocation) {
|
||||||
|
// Keep default greeting disabled during reception even before arriving at spot.
|
||||||
|
return true
|
||||||
|
}
|
||||||
when (state) {
|
when (state) {
|
||||||
DETECTED -> {
|
DETECTED -> {
|
||||||
startTaskWaitTimeout()
|
startTaskWaitTimeout()
|
||||||
@@ -194,8 +214,9 @@ class TaskController(
|
|||||||
setReceptionButtonVisible(false)
|
setReceptionButtonVisible(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return currentTask == "reception"
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun endNonSpecialTask(reason: String) {
|
fun endNonSpecialTask(reason: String) {
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package com.example.lzwcai_terminal_temi
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class WorkflowService(
|
||||||
|
private val context: Context,
|
||||||
|
private val prefs: SharedPreferences,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val normalizeLocation: (String?) -> String,
|
||||||
|
private val onWorkflowFailed: () -> Unit
|
||||||
|
) {
|
||||||
|
private var pendingReceptionReturnWorkflow: Boolean = false
|
||||||
|
private var lastWorkflowConfigRefreshAt: Long = 0L
|
||||||
|
|
||||||
|
fun markReceptionReturnPending() {
|
||||||
|
pendingReceptionReturnWorkflow = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun triggerReceptionReturnWorkflowIfNeeded(location: String) {
|
||||||
|
if (!pendingReceptionReturnWorkflow) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (normalizeLocation(location) != "homebase") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pendingReceptionReturnWorkflow = false
|
||||||
|
scope.launch {
|
||||||
|
val nowText = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())
|
||||||
|
val inputs = mapOf("flag" to nowText)
|
||||||
|
val result = executeConfiguredWorkflow(
|
||||||
|
workflowIdKey = HttpManager.PREF_KEY_VR_WFID,
|
||||||
|
workflowApiKey = HttpManager.PREF_KEY_VR_WF_KEY,
|
||||||
|
workflowName = "reception-return-home",
|
||||||
|
inputs = inputs
|
||||||
|
)
|
||||||
|
if (result == null) {
|
||||||
|
onWorkflowFailed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun executeDoorWorkflow(openDoor: Boolean): String? {
|
||||||
|
val workflowIdKey = if (openDoor) HttpManager.PREF_KEY_OD_WFID else HttpManager.PREF_KEY_CD_WFID
|
||||||
|
val workflowApiKey = if (openDoor) HttpManager.PREF_KEY_OD_WF_KEY else HttpManager.PREF_KEY_CD_WF_KEY
|
||||||
|
val workflowName = if (openDoor) "open-door" else "close-door"
|
||||||
|
return executeConfiguredWorkflow(workflowIdKey, workflowApiKey, workflowName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun executeConfiguredWorkflow(
|
||||||
|
workflowIdKey: String,
|
||||||
|
workflowApiKey: String,
|
||||||
|
workflowName: String,
|
||||||
|
inputs: Any = emptyMap<String, Any>()
|
||||||
|
): String? {
|
||||||
|
var workflowId = prefs.getString(workflowIdKey, "").orEmpty().trim()
|
||||||
|
var apiKey = prefs.getString(workflowApiKey, "").orEmpty().trim()
|
||||||
|
if (workflowId.isEmpty() || apiKey.isEmpty()) {
|
||||||
|
refreshWorkflowConfigsIfNeeded()
|
||||||
|
workflowId = prefs.getString(workflowIdKey, "").orEmpty().trim()
|
||||||
|
apiKey = prefs.getString(workflowApiKey, "").orEmpty().trim()
|
||||||
|
}
|
||||||
|
if (workflowId.isEmpty() || apiKey.isEmpty()) {
|
||||||
|
Log.w(
|
||||||
|
"MainActivity",
|
||||||
|
"Workflow config missing after refresh: workflow=$workflowName, " +
|
||||||
|
"workflowIdKey=$workflowIdKey, workflowApiKey=$workflowApiKey"
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return HttpManager.workflow_execute(
|
||||||
|
context = context,
|
||||||
|
apiKey = apiKey,
|
||||||
|
workflowId = workflowId,
|
||||||
|
inputs = inputs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun refreshWorkflowConfigsIfNeeded() {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now - lastWorkflowConfigRefreshAt < 5000L) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastWorkflowConfigRefreshAt = now
|
||||||
|
val runtimeConfigs = HttpManager.fetchRuntimeConfigs(context) ?: return
|
||||||
|
val workflowKeys = listOf(
|
||||||
|
HttpManager.PREF_KEY_OD_WFID,
|
||||||
|
HttpManager.PREF_KEY_OD_WF_KEY,
|
||||||
|
HttpManager.PREF_KEY_CD_WFID,
|
||||||
|
HttpManager.PREF_KEY_CD_WF_KEY,
|
||||||
|
HttpManager.PREF_KEY_VR_WFID,
|
||||||
|
HttpManager.PREF_KEY_VR_WF_KEY
|
||||||
|
)
|
||||||
|
val editor = prefs.edit()
|
||||||
|
var changed = false
|
||||||
|
for (key in workflowKeys) {
|
||||||
|
val value = runtimeConfigs[key]?.trim().orEmpty()
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (prefs.getString(key, "").orEmpty() != value) {
|
||||||
|
editor.putString(key, value)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
editor.apply()
|
||||||
|
Log.i("MainActivity", "Workflow configs refreshed from server.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
android:layout_marginEnd="40dp"
|
android:layout_marginEnd="40dp"
|
||||||
android:layout_marginTop="40dp"
|
android:layout_marginTop="40dp"
|
||||||
android:layout_marginBottom="40dp"
|
android:layout_marginBottom="40dp"
|
||||||
android:text="是的,请带我去"
|
android:text="带我去接待地点"
|
||||||
android:textSize="48sp"
|
android:textSize="48sp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
android:layout_marginEnd="40dp"
|
android:layout_marginEnd="40dp"
|
||||||
android:layout_marginTop="40dp"
|
android:layout_marginTop="40dp"
|
||||||
android:layout_marginBottom="40dp"
|
android:layout_marginBottom="40dp"
|
||||||
android:text="是的,请带我去"
|
android:text="带我去接待地点"
|
||||||
android:textSize="48sp"
|
android:textSize="48sp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
|||||||
Reference in New Issue
Block a user