diff --git a/README.md b/README.md index ba5e82c..549efb7 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,27 @@ Temi SDK 的很多功能(如语音、导航、跟随)依赖于机器人的 - `app/src/main/res/layout/activity_main.xml` (主页布局) - `app/src/main/res/layout/activity_settings.xml` (设置页布局) -## 4. 常见问题 +## 4. MQTT 指令与行为 -**Q: 为什么在模拟器上闪退?** -A: 因为应用启动时会调用 `Robot.getInstance()`,而普通模拟器没有 Temi 的底层服务。 +应用订阅 `robot/cmd`,接收 JSON 指令。 + +### 动作列表 +- `recharge` 前往充电桩 +- `goto` 前往指定地点(字段 `location` 或 `target`) +- `speak` 立即播报(字段 `text` 或 `speech`) +- `stream` 流式播报(字段 `text` 或 `content`),按句号/感叹号/问号/换行分句 +- `stop` 暂停 TTS 与播报队列,不清空 stream buffer +- `continue` 继续播报,优先重播被中断的那句话 +- `terminate` 终止导航与 TTS,清空队列和 buffer + +### special 模式说明 +- special 是否启用只看设置页开关 `special_task_mode` +- setCurrentTask 不会开启或关闭 special +- special 开启时,会跳过门控与问候等场景逻辑 + +## 5. 本地验证 + +```bash +.\gradlew.bat :app:installDebug +``` -**Q: Trae 可以装插件预览吗?** -A: 目前没有插件能直接在 Trae 内部完美模拟 Android 环境。建议使用 Trae 写代码,配合 Android Studio 或真机进行调试预览。 diff --git a/app/src/main/java/com/example/lzwcai_terminal_temi/AnimatedEmojiView.kt b/app/src/main/java/com/example/lzwcai_terminal_temi/AnimatedEmojiView.kt index b312c90..1440a07 100644 --- a/app/src/main/java/com/example/lzwcai_terminal_temi/AnimatedEmojiView.kt +++ b/app/src/main/java/com/example/lzwcai_terminal_temi/AnimatedEmojiView.kt @@ -17,7 +17,7 @@ class AnimatedEmojiView @JvmOverloads constructor( ) : View(context, attrs) { private val facePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = Color.YELLOW + color = Color.WHITE style = Paint.Style.FILL } @@ -39,7 +39,7 @@ class AnimatedEmojiView @JvmOverloads constructor( private var mouthOpenRatio = 0.1f private var noddingOffset = 0f - enum class Expression { SMILE, NEUTRAL, TALKING, HAPPY, SAD, WINK, ANGRY } + enum class Expression { SMILE, NEUTRAL, TALKING, HAPPY, SAD, WINK, ANGRY, BLINK } var currentExpression = Expression.SMILE set(value) { field = value @@ -70,12 +70,19 @@ class AnimatedEmojiView @JvmOverloads constructor( val eyeRadius = radius * 0.1f val eyeOffsetX = radius * 0.4f val eyeOffsetY = radius * 0.3f + val closedEyeWidth = eyeRadius * 2.5f if (currentExpression == Expression.WINK) { - val closedEyeWidth = eyeRadius * 2.5f mouthPaint.style = Paint.Style.STROKE + // Left eye closed (wink) + canvas.drawLine(centerX - eyeOffsetX - closedEyeWidth / 2, centerY - eyeOffsetY, centerX - eyeOffsetX + closedEyeWidth / 2, centerY - eyeOffsetY, mouthPaint) + // Right eye open + canvas.drawCircle(centerX + eyeOffsetX, centerY - eyeOffsetY, eyeRadius, eyePaint) + } else if (currentExpression == Expression.BLINK) { + mouthPaint.style = Paint.Style.STROKE + // Both eyes closed + canvas.drawLine(centerX - eyeOffsetX - closedEyeWidth / 2, centerY - eyeOffsetY, centerX - eyeOffsetX + closedEyeWidth / 2, centerY - eyeOffsetY, mouthPaint) canvas.drawLine(centerX + eyeOffsetX - closedEyeWidth / 2, centerY - eyeOffsetY, centerX + eyeOffsetX + closedEyeWidth / 2, centerY - eyeOffsetY, mouthPaint) - canvas.drawCircle(centerX - eyeOffsetX, centerY - eyeOffsetY, eyeRadius, eyePaint) } else { canvas.drawCircle(centerX - eyeOffsetX, centerY - eyeOffsetY, eyeRadius, eyePaint) canvas.drawCircle(centerX + eyeOffsetX, centerY - eyeOffsetY, eyeRadius, eyePaint) @@ -121,6 +128,7 @@ class AnimatedEmojiView @JvmOverloads constructor( Expression.ANGRY -> canvas.drawArc(mouthPath, 200f, -140f, false, mouthPaint) Expression.NEUTRAL -> canvas.drawLine(mouthLeft, mouthTop + mouthHeight / 2, mouthLeft + mouthWidth, mouthTop + mouthHeight / 2, mouthPaint) Expression.WINK -> canvas.drawArc(mouthPath, 20f, 140f, false, mouthPaint) + Expression.BLINK -> canvas.drawArc(mouthPath, 20f, 140f, false, mouthPaint) Expression.TALKING -> { mouthPaint.style = Paint.Style.FILL val dynamicMouthHeight = mouthHeight * mouthOpenRatio diff --git a/app/src/main/java/com/example/lzwcai_terminal_temi/MainActivity.kt b/app/src/main/java/com/example/lzwcai_terminal_temi/MainActivity.kt index acb66f6..8f4d75b 100644 --- a/app/src/main/java/com/example/lzwcai_terminal_temi/MainActivity.kt +++ b/app/src/main/java/com/example/lzwcai_terminal_temi/MainActivity.kt @@ -27,6 +27,8 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlin.random.Random class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnGoToLocationStatusChangedListener, OnDetectionStateChangedListener, OnReposeStatusChangedListener, SharedPreferences.OnSharedPreferenceChangeListener, @@ -42,16 +44,18 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG private var lastArrivalLocation: String? = null private var lastArrivalAt: Long = 0L - private val fixedFaceScale = 1.0f - private val baseFaceSizeDp = 1000f + private var currentTask: String = "" private var closeDoorJob: Job? = null + private var blinkJob: Job? = null private var receptionLocation: String = "" private var receptionText: String = "" private var receptionDestination: String = "" + private var patrolRoute: List = emptyList() + private var patrolIndex: Int = 0 @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { @@ -65,17 +69,31 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG navCon = NavController(robot) permissionManager = PermissionManager(robot) - prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE) - if (prefs.getBoolean("special_task_mode", false)) { - currentTask = "special" + if (savedInstanceState != null) { + currentTask = savedInstanceState.getString("currentTask", "") ?: "" + receptionLocation = savedInstanceState.getString("receptionLocation", "") ?: "" + receptionText = savedInstanceState.getString("receptionText", "") ?: "" + receptionDestination = savedInstanceState.getString("receptionDestination", "") ?: "" + lastArrivalLocation = savedInstanceState.getString("lastArrivalLocation") + } + if (currentTask == "special") { + currentTask = "" } + prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE) + prefs.registerOnSharedPreferenceChangeListener(this) + if (lastArrivalLocation == null) { + lastArrivalLocation = prefs.getString("current_location", null) + } binding.btnSettings.setOnClickListener { startActivity(Intent(this, SettingsActivity::class.java)) } - binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE - // applyFaceScale(fixedFaceScale) // Use XML constraints for layout + if (currentTask == "patrol") { + binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.ANGRY + } else { + binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE + } binding.btnReception.setOnClickListener { val destination = receptionDestination @@ -96,9 +114,13 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG robot.addOnDetectionStateChangedListener(this) robot.addOnReposeStatusChangedListener(this) robot.addOnRequestPermissionResultListener(this) - prefs.registerOnSharedPreferenceChangeListener(this) robot.constraintBeWith() - mqttManager?.connect() + if (mqttManager == null) { + updateMqttConnection() + } else { + mqttManager?.connect() + } + startBlinking() } override fun onStop() { @@ -109,12 +131,13 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG robot.removeOnDetectionStateChangedListener(this) robot.removeOnReposeStatusChangedListener(this) robot.removeOnRequestPermissionResultListener(this) - prefs.unregisterOnSharedPreferenceChangeListener(this) - mqttManager?.disconnect() + // mqttManager?.disconnect() // Keep MQTT alive in background/settings + stopBlinking() } override fun onDestroy() { super.onDestroy() + prefs.unregisterOnSharedPreferenceChangeListener(this) mqttManager?.disconnect() LogManager.stopLogcatListener() mainScope.cancel() @@ -168,7 +191,12 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG override fun onGoToLocationStatusChanged(location: String, status: String, descriptionId: Int, description: String) { val normalized = status.lowercase() - if (normalized != "complete") { + val isAbort = normalized in setOf("abort", "aborted", "canceled", "cancelled", "stopped", "stop", "failed", "error") + if (normalized != "complete" && !isAbort) { + return + } + if (isAbort) { + endNonSpecialTask("goTo aborted: $location, status=$status") return } val now = System.currentTimeMillis() @@ -177,6 +205,14 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG } lastArrivalLocation = location lastArrivalAt = now + prefs.edit().putString("current_location", location).apply() + if (currentTask == "patrol") { + handlePatrolArrival(location) + } + if (isSpecialModeEnabled() && currentTask.isEmpty()) { + Log.i("MainActivity", "Special task mode: arrival announcement skipped at $location.") + return + } val text = "已到达$location" val ttsRequest = TtsRequest.create(text, false, language = TtsRequest.Language.ZH_CN) robot.speak(ttsRequest) @@ -209,6 +245,12 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG // Home Base logic if (lastArrivalLocation?.lowercase() == "home base") { + // Check if special task mode is enabled, if so, skip door logic + if (isSpecialModeEnabled() && currentTask.isEmpty()) { + Log.i("MainActivity", "Special task mode: Door logic skipped at Home Base.") + return + } + when (state) { DETECTED -> { closeDoorJob?.cancel() @@ -241,8 +283,11 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG } } } - - if (state == DETECTED && currentTask.isEmpty() && lastArrivalLocation?.lowercase() != "home base") { + if (lastArrivalLocation?.lowercase() != "home base" && currentTask.isEmpty() && state == DETECTED) { + if (isSpecialModeEnabled()) { + Log.i("MainActivity", "Special task mode enabled (pref check), skipping greeting.") + return + } val hour = java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY) val greeting = when (hour) { in 6..11 -> "早上好" @@ -267,7 +312,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG } private fun stopReceptionMode() { - currentTask = "" + setCurrentTask("") receptionLocation = "" receptionText = "" receptionDestination = "" @@ -275,15 +320,64 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG Log.i("MainActivity", "Reception mode stopped") } + private fun endNonSpecialTask(reason: String) { + if (currentTask.isEmpty()) { + return + } + if (currentTask != "patrol" && currentTask != "reception") { + return + } + Log.i("MainActivity", "Ending task '$currentTask': $reason") + setCurrentTask("") + } + + fun startPatrolMode(route: List) { + if (route.isEmpty()) { + setCurrentTask("") + return + } + patrolRoute = route + patrolIndex = 0 + setCurrentTask("patrol") + Log.i("MainActivity", "Patrol mode started: route=${route.joinToString()}") + } + + private fun handlePatrolArrival(location: String) { + if (patrolRoute.isEmpty()) { + return + } + val matchIndex = patrolRoute.indexOfFirst { it.equals(location, ignoreCase = true) } + if (matchIndex == -1) { + return + } + if (matchIndex >= patrolIndex) { + patrolIndex = matchIndex + 1 + } + if (patrolIndex >= patrolRoute.size) { + Log.i("MainActivity", "Patrol route completed: ${patrolRoute.joinToString()}") + setCurrentTask("") + } + } + fun setCurrentTask(task: String) { - currentTask = task - Log.i("MainActivity", "Current task set to: $task") - // Update expression based on task - if (task == "patrol") { + val finalTask = task + + // Avoid re-setting the same task + if (currentTask == finalTask) { + return + } + + currentTask = finalTask + Log.i("MainActivity", "Current task set to: '$finalTask'") + if (finalTask == "patrol") { binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.ANGRY } else { binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE } + if (finalTask != "patrol") { + patrolRoute = emptyList() + patrolIndex = 0 + } } override fun onReposeStatusChanged(status: Int, description: String) { @@ -311,17 +405,22 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG Log.i("MainActivity", "IP address changed, re-initializing MQTT connection.") updateMqttConnection() } + if (key == "current_location") { + lastArrivalLocation = sharedPreferences?.getString("current_location", null) + Log.i("MainActivity", "Current location updated manually: $lastArrivalLocation") + } if (key == "special_task_mode") { - if (sharedPreferences?.getBoolean("special_task_mode", false) == true) { - setCurrentTask("special") - } else if (currentTask == "special") { - setCurrentTask("") - } + val isSpecial = sharedPreferences?.getBoolean("special_task_mode", false) == true + Log.i("MainActivity", "Special mode pref changed: $isSpecial, currentTask: $currentTask") } } + private fun isSpecialModeEnabled(): Boolean { + return ::prefs.isInitialized && prefs.getBoolean("special_task_mode", false) + } + private fun updateMqttConnection() { - mqttManager?.disconnect() + mqttManager?.shutdown() val ip = prefs.getString("network_ip", null) if (!ip.isNullOrEmpty()) { mqttManager = MqttManager(this, ip, robot, navCon) @@ -333,14 +432,35 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG } } - 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() + private fun startBlinking() { + stopBlinking() + blinkJob = mainScope.launch { + while (isActive) { + val delayTime = Random.nextLong(2000, 6000) + delay(delayTime) + + if (binding.animatedEmojiView.currentExpression == AnimatedEmojiView.Expression.SMILE) { + binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.BLINK + delay(200) + if (binding.animatedEmojiView.currentExpression == AnimatedEmojiView.Expression.BLINK) { + binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE + } + } + } + } + } + + private fun stopBlinking() { + blinkJob?.cancel() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString("currentTask", currentTask) + outState.putString("receptionLocation", receptionLocation) + outState.putString("receptionText", receptionText) + outState.putString("receptionDestination", receptionDestination) + outState.putString("lastArrivalLocation", lastArrivalLocation) } } diff --git a/app/src/main/java/com/example/lzwcai_terminal_temi/MqttManager.kt b/app/src/main/java/com/example/lzwcai_terminal_temi/MqttManager.kt index 003e5c9..890f34e 100644 --- a/app/src/main/java/com/example/lzwcai_terminal_temi/MqttManager.kt +++ b/app/src/main/java/com/example/lzwcai_terminal_temi/MqttManager.kt @@ -10,7 +10,7 @@ import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence import org.json.JSONObject import java.util.LinkedList import java.util.Queue - +import java.nio.charset.StandardCharsets class MqttManager( private val context: Context, private val serverIp: String, @@ -32,6 +32,13 @@ class MqttManager( // TTS Queue private val ttsQueue: Queue = LinkedList() private var isTtsBusy = false + private var isTtsPaused = false + private var currentTtsSpeech: String? = null + private var currentTtsLanguage: TtsRequest.Language? = null + private var interruptedSpeech: String? = null + private var interruptedLanguage: TtsRequest.Language? = null + private var lastStreamLangCode: String? = null + private val ttsLanguageMap = mutableMapOf() init { try { @@ -48,9 +55,11 @@ class MqttManager( } override fun messageArrived(topic: String?, message: MqttMessage?) { - val payload = String(message?.payload ?: ByteArray(0)) + val payload = message?.let { String(it.payload, StandardCharsets.UTF_8) } Log.i(TAG, "Message arrived: Topic=$topic, Payload=$payload") - handleIncomingMessage(topic, payload) + if (payload != null) { + handleIncomingMessage(topic, payload) + } } override fun deliveryComplete(token: IMqttDeliveryToken?) { @@ -110,6 +119,21 @@ class MqttManager( } } + fun shutdown() { + reconnectJob?.cancel() + job.cancel() + try { + if (mqttClient?.isConnected == true) { + mqttClient?.disconnect() + } + mqttClient?.close() + } catch (e: MqttException) { + Log.e(TAG, "Error shutting down MQTT client: ${e.message}") + } finally { + mqttClient = null + } + } + private fun subscribeTopic(topic: String) { try { if (mqttClient?.isConnected == true) { @@ -154,7 +178,9 @@ class MqttManager( val obj = JSONObject(trimmed) handleJsonCommand(obj) } catch (e: Exception) { - Log.w(TAG, "Invalid JSON payload: $payload") + Log.e(TAG, "Invalid JSON payload: $payload", e) + val ttsRequest = TtsRequest.create("指令格式错误,请检查指令格式", false, language = TtsRequest.Language.ZH_CN) + robot.speak(ttsRequest) } } @@ -184,17 +210,29 @@ class MqttManager( Log.i(TAG, "Repose command sent: $ok") } "stop" -> { - (context as? MainActivity)?.setCurrentTask("") + scope.launch(Dispatchers.Main) { + (context as? MainActivity)?.setCurrentTask("") + } + navController.stop() + pauseTts() + } + "terminate" -> { + scope.launch(Dispatchers.Main) { + (context as? MainActivity)?.setCurrentTask("") + } navController.stop() stopTts() } + "continue" -> { + resumeTts() + } "patrol" -> { - (context as? MainActivity)?.setCurrentTask("patrol") speak("接到巡逻任务", "zh") val flag = obj.optBoolean("flag", true) + var patrolLocations: List = emptyList() if (flag) { Log.d(TAG, "navController.randomPatrol() called.") - navController.randomPatrol() + patrolLocations = navController.randomPatrol() } else { val locationsArray = obj.optJSONArray("locations") if (locationsArray != null && locationsArray.length() > 0) { @@ -203,25 +241,41 @@ class MqttManager( } Log.d(TAG, "navController.NavPatrol() called with locations: $locations") navController.NavPatrol(locations) + patrolLocations = locations } else { Log.w(TAG, "Patrol command received without locations, falling back to random patrol.") - navController.randomPatrol() + patrolLocations = navController.randomPatrol() + } + } + scope.launch(Dispatchers.Main) { + val activity = context as? MainActivity + if (patrolLocations.isNotEmpty()) { + activity?.startPatrolMode(patrolLocations) + } else { + activity?.setCurrentTask("") } } } "reception" -> { + speak("接到接待任务", "zh") val location = obj.optString("location", "前台") val text = obj.optString("text", "你是我要接待的贵宾吗?") val destination = obj.optString("destination", "会议室") - (context as? MainActivity)?.startReceptionMode(location, text, destination) + scope.launch(Dispatchers.Main) { + (context as? MainActivity)?.startReceptionMode(location, text, destination) + } } else -> Log.w(TAG, "Unknown command action: $action") } } private fun processStreamText(text: String, langCode: String?) { + lastStreamLangCode = langCode speechBuffer.append(text) - + if (isTtsPaused) { + return + } + while (true) { val content = speechBuffer.toString() var minIndex = -1 @@ -252,8 +306,25 @@ class MqttManager( } } + private fun pauseTts() { + isTtsPaused = true + interruptedSpeech = currentTtsSpeech + interruptedLanguage = currentTtsLanguage + scope.launch(Dispatchers.Main) { + robot.cancelAllTtsRequests() + Log.i(TAG, "TTS paused.") + } + } + private fun stopTts() { + isTtsPaused = false + interruptedSpeech = null + interruptedLanguage = null + currentTtsSpeech = null + currentTtsLanguage = null + lastStreamLangCode = null speechBuffer.setLength(0) + ttsLanguageMap.clear() scope.launch(Dispatchers.Main) { ttsQueue.clear() isTtsBusy = false @@ -262,6 +333,39 @@ class MqttManager( } } + private fun resumeTts() { + if (!isTtsPaused) { + return + } + isTtsPaused = false + val replaySpeech = interruptedSpeech?.trim().orEmpty() + val replayLanguage = interruptedLanguage + interruptedSpeech = null + interruptedLanguage = null + scope.launch(Dispatchers.Main) { + if (replaySpeech.isNotEmpty()) { + val language = replayLanguage ?: resolveLanguage(lastStreamLangCode) + val replayRequest = TtsRequest.create(replaySpeech, false, language = language) + ttsLanguageMap[replayRequest] = language + if (!isTtsBusy) { + isTtsBusy = true + robot.speak(replayRequest) + Log.i(TAG, "Resume speak: $replaySpeech") + } else { + val list = ttsQueue as? LinkedList + if (list != null) { + list.addFirst(replayRequest) + } else { + ttsQueue.offer(replayRequest) + } + Log.i(TAG, "Resume queued: $replaySpeech") + } + } + processStreamText("", lastStreamLangCode) + processNextTts() + } + } + private fun goTo(location: String, backwards: Boolean = false) { val target = location.trim() if (target.isEmpty()) { @@ -280,7 +384,14 @@ class MqttManager( val language = resolveLanguage(langCode) scope.launch(Dispatchers.Main) { val ttsRequest = TtsRequest.create(content, false, language = language) + ttsLanguageMap[ttsRequest] = language + if (isTtsPaused) { + ttsQueue.offer(ttsRequest) + Log.i(TAG, "Speak queued (paused): $content. Queue size: ${ttsQueue.size}") + return@launch + } + if (!isTtsBusy) { isTtsBusy = true robot.speak(ttsRequest) @@ -297,13 +408,19 @@ class MqttManager( when (ttsRequest.status) { TtsRequest.Status.STARTED -> { isTtsBusy = true + currentTtsSpeech = ttsRequest.speech + currentTtsLanguage = ttsLanguageMap[ttsRequest] } TtsRequest.Status.COMPLETED, TtsRequest.Status.CANCELED, TtsRequest.Status.ERROR, TtsRequest.Status.NOT_ALLOWED -> { isTtsBusy = false - processNextTts() + currentTtsSpeech = null + currentTtsLanguage = null + if (!isTtsPaused) { + processNextTts() + } } else -> {} } @@ -311,7 +428,7 @@ class MqttManager( } private fun processNextTts() { - if (!isTtsBusy && ttsQueue.isNotEmpty()) { + if (!isTtsBusy && !isTtsPaused && ttsQueue.isNotEmpty()) { val next = ttsQueue.poll() if (next != null) { isTtsBusy = true diff --git a/app/src/main/java/com/example/lzwcai_terminal_temi/NavController.kt b/app/src/main/java/com/example/lzwcai_terminal_temi/NavController.kt index ec9496a..71e0065 100644 --- a/app/src/main/java/com/example/lzwcai_terminal_temi/NavController.kt +++ b/app/src/main/java/com/example/lzwcai_terminal_temi/NavController.kt @@ -40,16 +40,17 @@ class NavController(private val robot: Robot) { return true } - fun randomPatrol() { + fun randomPatrol(): List { val allLocations = getAllLocations() 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 emptyList() } val patrolCount = (3..minOf(6, availablePatrolLocations.size)).random() val patrolLocations = availablePatrolLocations.shuffled().take(patrolCount) Log.i(TAG, "Starting random patrol with $patrolCount locations: ${patrolLocations.joinToString()}.") NavPatrol(patrolLocations, false, 1, 5) + return patrolLocations } } diff --git a/app/src/main/java/com/example/lzwcai_terminal_temi/SettingsActivity.kt b/app/src/main/java/com/example/lzwcai_terminal_temi/SettingsActivity.kt index cdc6720..f7ec1d4 100644 --- a/app/src/main/java/com/example/lzwcai_terminal_temi/SettingsActivity.kt +++ b/app/src/main/java/com/example/lzwcai_terminal_temi/SettingsActivity.kt @@ -12,10 +12,12 @@ import android.view.MotionEvent import android.view.View import android.view.WindowManager import android.view.inputmethod.InputMethodManager +import android.widget.ArrayAdapter import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import com.example.lzwcai_terminal_temi.databinding.ActivitySettingsBinding import kotlin.system.exitProcess +import com.robotemi.sdk.Robot import android.graphics.drawable.GradientDrawable import androidx.core.content.ContextCompat @@ -23,6 +25,9 @@ class SettingsActivity : AppCompatActivity() { private lateinit var binding: ActivitySettingsBinding private var restartAnimator: ValueAnimator? = null + private lateinit var robot: Robot + private lateinit var locationAdapter: ArrayAdapter + private val currentLocationKey = "current_location" private lateinit var prefs: SharedPreferences override fun onCreate(savedInstanceState: Bundle?) { @@ -31,6 +36,7 @@ class SettingsActivity : AppCompatActivity() { setContentView(binding.root) window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) + robot = Robot.getInstance() prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE) val savedIp = prefs.getString("network_ip", "") @@ -48,7 +54,6 @@ class SettingsActivity : AppCompatActivity() { if (ip.isNotEmpty()) { prefs.edit().putString("network_ip", ip).apply() Toast.makeText(this, getString(R.string.msg_ip_saved, ip), Toast.LENGTH_SHORT).show() - Log.i("SettingsActivity", "IP Saved: $ip") finish() } else { Toast.makeText(this, getString(R.string.msg_invalid_ip), Toast.LENGTH_SHORT).show() @@ -63,16 +68,23 @@ class SettingsActivity : AppCompatActivity() { setupRestartButton() setupSpecialTaskSwitch() + setupLocationSelector() + } + + override fun onResume() { + super.onResume() + refreshLocationList() } private fun setupSpecialTaskSwitch() { val isSpecialTaskMode = prefs.getBoolean("special_task_mode", false) + binding.switchSpecialTask.setOnCheckedChangeListener(null) binding.switchSpecialTask.isChecked = isSpecialTaskMode updateStatusIndicator(isSpecialTaskMode) - binding.switchSpecialTask.setOnCheckedChangeListener { _, isChecked -> prefs.edit().putBoolean("special_task_mode", isChecked).apply() updateStatusIndicator(isChecked) + Log.i("SettingsActivity", "Special Task Mode changed to: $isChecked") } } @@ -130,6 +142,45 @@ class SettingsActivity : AppCompatActivity() { binding.restartProgressBar.visibility = View.INVISIBLE } + private fun setupLocationSelector() { + val items = buildLocationList() + locationAdapter = ArrayAdapter(this, R.layout.item_dropdown, items) + binding.etCurrentLocation.setAdapter(locationAdapter) + val selection = resolveCurrentLocation(items) + binding.etCurrentLocation.setText(selection, false) + binding.etCurrentLocation.setOnItemClickListener { _, _, position, _ -> + val selected = locationAdapter.getItem(position) ?: getString(R.string.location_unknown) + prefs.edit().putString(currentLocationKey, selected).apply() + } + } + + private fun refreshLocationList() { + if (!::locationAdapter.isInitialized) { + return + } + val items = buildLocationList() + locationAdapter.clear() + locationAdapter.addAll(items) + locationAdapter.notifyDataSetChanged() + val selection = resolveCurrentLocation(items) + binding.etCurrentLocation.setText(selection, false) + } + + private fun resolveCurrentLocation(items: List): String { + val unknown = getString(R.string.location_unknown) + val saved = prefs.getString(currentLocationKey, unknown) ?: unknown + return if (items.contains(saved)) saved else unknown + } + + private fun buildLocationList(): List { + val unknown = getString(R.string.location_unknown) + val locations = runCatching { robot.locations }.getOrDefault(emptyList()) + val unique = LinkedHashSet() + unique.add(unknown) + locations.map { it.trim() }.filter { it.isNotEmpty() }.forEach { unique.add(it) } + return unique.toList() + } + private fun restartApplication() { val intent = packageManager.getLaunchIntentForPackage(packageName) val pendingIntent = PendingIntent.getActivity(this, 123456, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE) diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml new file mode 100644 index 0000000..0ac98d7 --- /dev/null +++ b/app/src/main/res/layout-land/activity_main.xml @@ -0,0 +1,52 @@ + + + + + + + +