From b15c5c902180ab62a998d7630419ad8f35813ab5 Mon Sep 17 00:00:00 2001 From: Sucan <632190820@qq.com> Date: Fri, 13 Mar 2026 15:27:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=89=B9=E6=AE=8A?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E6=A8=A1=E5=BC=8F=E3=80=81=E7=94=9F=E6=B0=94?= =?UTF-8?q?=E8=A1=A8=E6=83=85=E5=92=8C=E7=89=88=E6=9C=AC=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增特殊任务模式开关,带状态指示灯 - 扩展表情系统,增加生气表情并支持眉毛绘制 - 在设置界面显示版本号 - 优化任务管理逻辑,支持特殊任务状态同步 - 改进导航指令命名,将充电功能重命名为 recharge - 增强人机交互,添加基于时间和任务的智能问候 - 优化家庭基站检测逻辑,添加开门/关门提示 --- .../lzwcai_terminal_temi/AnimatedEmojiView.kt | 24 +- .../lzwcai_terminal_temi/MainActivity.kt | 115 +++++--- .../lzwcai_terminal_temi/MqttManager.kt | 5 +- .../lzwcai_terminal_temi/NavController.kt | 2 +- .../lzwcai_terminal_temi/SettingsActivity.kt | 32 ++- .../main/res/drawable/custom_progress_bar.xml | 4 +- .../main/res/drawable/status_indicator.xml | 6 + app/src/main/res/layout/activity_main.xml | 71 +++-- app/src/main/res/layout/activity_settings.xml | 251 +++++++++++++----- app/src/main/res/values-night/themes.xml | 58 +++- app/src/main/res/values/colors.xml | 11 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/themes.xml | 59 +++- 13 files changed, 491 insertions(+), 148 deletions(-) create mode 100644 app/src/main/res/drawable/status_indicator.xml 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 c68e879..b312c90 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 @@ -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 } + enum class Expression { SMILE, NEUTRAL, TALKING, HAPPY, SAD, WINK, ANGRY } var currentExpression = Expression.SMILE set(value) { field = value @@ -81,6 +81,27 @@ class AnimatedEmojiView @JvmOverloads constructor( canvas.drawCircle(centerX + eyeOffsetX, centerY - eyeOffsetY, eyeRadius, eyePaint) } + // 2.1 画眉毛 (仅在生气时) + if (currentExpression == Expression.ANGRY) { + val eyebrowWidth = eyeRadius * 2.5f + val eyebrowOffsetY = eyeOffsetY + eyeRadius * 1.5f + mouthPaint.strokeWidth = 10f // 眉毛可以细一点 + + // 左眉毛 + canvas.save() + canvas.rotate(15f, centerX - eyeOffsetX, centerY - eyebrowOffsetY) + canvas.drawLine(centerX - eyeOffsetX - eyebrowWidth / 2, centerY - eyebrowOffsetY, centerX - eyeOffsetX + eyebrowWidth / 2, centerY - eyebrowOffsetY, mouthPaint) + canvas.restore() + + // 右眉毛 + canvas.save() + canvas.rotate(-15f, centerX + eyeOffsetX, centerY - eyebrowOffsetY) + canvas.drawLine(centerX + eyeOffsetX - eyebrowWidth / 2, centerY - eyebrowOffsetY, centerX + eyeOffsetX + eyebrowWidth / 2, centerY - eyebrowOffsetY, mouthPaint) + canvas.restore() + + mouthPaint.strokeWidth = 15f // 恢复嘴巴的宽度 + } + // 3. 画嘴巴 mouthPaint.style = Paint.Style.STROKE val mouthWidth = radius * 0.6f @@ -97,6 +118,7 @@ class AnimatedEmojiView @JvmOverloads constructor( canvas.drawArc(happyMouthPath, 0f, 180f, false, mouthPaint) } Expression.SAD -> canvas.drawArc(mouthPath, 200f, -140f, false, mouthPaint) + 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.TALKING -> { 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 3b73f03..acb66f6 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 @@ -25,6 +25,9 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.cancel +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay + class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnGoToLocationStatusChangedListener, OnDetectionStateChangedListener, OnReposeStatusChangedListener, SharedPreferences.OnSharedPreferenceChangeListener, OnRequestPermissionResultListener { @@ -43,6 +46,9 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG private val baseFaceSizeDp = 1000f private var currentTask: String = "" + + private var closeDoorJob: Job? = null + private var receptionLocation: String = "" private var receptionText: String = "" private var receptionDestination: String = "" @@ -60,13 +66,16 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG permissionManager = PermissionManager(robot) prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE) + if (prefs.getBoolean("special_task_mode", false)) { + currentTask = "special" + } binding.btnSettings.setOnClickListener { startActivity(Intent(this, SettingsActivity::class.java)) } binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE - applyFaceScale(fixedFaceScale) + // applyFaceScale(fixedFaceScale) // Use XML constraints for layout binding.btnReception.setOnClickListener { val destination = receptionDestination @@ -88,6 +97,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG robot.addOnReposeStatusChangedListener(this) robot.addOnRequestPermissionResultListener(this) prefs.registerOnSharedPreferenceChangeListener(this) + robot.constraintBeWith() mqttManager?.connect() } @@ -139,13 +149,14 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG Log.i("MainActivity", "TTS started") binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.TALKING } - TtsRequest.Status.COMPLETED -> { - Log.i("MainActivity", "TTS completed: ${ttsRequest.speech}") - binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE - } + TtsRequest.Status.COMPLETED, TtsRequest.Status.CANCELED -> { - Log.w("MainActivity", "TTS canceled: ${ttsRequest.speech}") - binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE + Log.i("MainActivity", "TTS finished: ${ttsRequest.speech}") + if (currentTask == "patrol") { + binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.ANGRY + } else { + binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE + } } TtsRequest.Status.ERROR -> { Log.e("MainActivity", "TTS error: ${ttsRequest.speech}") @@ -173,6 +184,12 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG } override fun onDetectionStateChanged(state: Int) { + if (currentTask == "patrol" && state == DETECTED) { + val ttsRequest = TtsRequest.create("别妨碍我,我正在巡逻呢", false, language = TtsRequest.Language.ZH_CN) + robot.speak(ttsRequest) + return + } + if (currentTask == "reception" && lastArrivalLocation == receptionLocation) { when (state) { DETECTED -> { @@ -191,34 +208,55 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG } // 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() - // ) - // } - // } - // IDLE -> { - // mainScope.launch { - // HttpManager.workflow_execute( - // context = this@MainActivity, - // apiKey = "wf_c02aa853371345dbb29572641d083c24", - // workflowId = "2031634633458520065", - // inputs = emptyMap() - // ) - // } - // } - // } - // } + if (lastArrivalLocation?.lowercase() == "home base") { + when (state) { + DETECTED -> { + closeDoorJob?.cancel() + binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.WINK + val ttsRequest = TtsRequest.create("正在为你开门,请稍等。", false, language = TtsRequest.Language.ZH_CN) + robot.speak(ttsRequest) + // mainScope.launch { + // HttpManager.workflow_execute( + // context = this@MainActivity, + // apiKey = "wf_865e80f5fc1a4a319474a21d47470863", + // workflowId = "2031297462423851009", + // inputs = emptyMap() + // ) + // } + } + IDLE -> { + closeDoorJob = mainScope.launch { + delay(5000) + val ttsRequest = TtsRequest.create("正准备关门,请小心被夹。", false, language = TtsRequest.Language.ZH_CN) + robot.speak(ttsRequest) + // mainScope.launch { + // HttpManager.workflow_execute( + // context = this@MainActivity, + // apiKey = "wf_c02aa853371345dbb29572641d083c24", + // workflowId = "2031634633458520065", + // inputs = emptyMap() + // ) + // } + } + } + } + } + + if (state == DETECTED && currentTask.isEmpty() && lastArrivalLocation?.lowercase() != "home base") { + val hour = java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY) + val greeting = when (hour) { + in 6..11 -> "早上好" + in 12..13 -> "中午好" + in 14..18 -> "下午好" + else -> "晚上好" + } + val ttsRequest = TtsRequest.create(greeting, false, language = TtsRequest.Language.ZH_CN) + robot.speak(ttsRequest) + } } fun startReceptionMode(location: String, text: String, destination: String) { - currentTask = "reception" + setCurrentTask("reception") receptionLocation = location receptionText = text receptionDestination = destination @@ -240,6 +278,12 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG fun setCurrentTask(task: String) { currentTask = task Log.i("MainActivity", "Current task set to: $task") + // Update expression based on task + if (task == "patrol") { + binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.ANGRY + } else { + binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE + } } override fun onReposeStatusChanged(status: Int, description: String) { @@ -267,6 +311,13 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG Log.i("MainActivity", "IP address changed, re-initializing MQTT connection.") updateMqttConnection() } + if (key == "special_task_mode") { + if (sharedPreferences?.getBoolean("special_task_mode", false) == true) { + setCurrentTask("special") + } else if (currentTask == "special") { + setCurrentTask("") + } + } } private fun updateMqttConnection() { 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 c793f99..003e5c9 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 @@ -161,9 +161,9 @@ class MqttManager( private fun handleJsonCommand(obj: JSONObject) { val action = obj.optString("action", obj.optString("cmd", obj.optString("type", ""))).lowercase() when (action) { - "charge" -> { + "recharge" -> { speak("前往充电桩", "zh") - navController.charge() + navController.recharge() } "goto" -> { val location = obj.optString("location", obj.optString("target", "")) @@ -184,6 +184,7 @@ class MqttManager( Log.i(TAG, "Repose command sent: $ok") } "stop" -> { + (context as? MainActivity)?.setCurrentTask("") navController.stop() stopTts() } 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 0515873..ec9496a 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 @@ -7,7 +7,7 @@ class NavController(private val robot: Robot) { private val TAG = "NavController" private var playmode = false - fun charge(): Boolean { + fun recharge(): Boolean { playmode = !playmode robot.goTo("home base", playmode) return true 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 5477968..cdc6720 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 @@ -5,6 +5,7 @@ import android.app.AlarmManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.content.SharedPreferences import android.os.Bundle import android.util.Log import android.view.MotionEvent @@ -15,11 +16,14 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import com.example.lzwcai_terminal_temi.databinding.ActivitySettingsBinding import kotlin.system.exitProcess +import android.graphics.drawable.GradientDrawable +import androidx.core.content.ContextCompat class SettingsActivity : AppCompatActivity() { private lateinit var binding: ActivitySettingsBinding private var restartAnimator: ValueAnimator? = null + private lateinit var prefs: SharedPreferences override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -28,10 +32,14 @@ class SettingsActivity : AppCompatActivity() { window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) - val prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE) + prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE) val savedIp = prefs.getString("network_ip", "") binding.etIpAddress.setText(savedIp) + // Set Version Name + val versionName = "2603121722" + binding.tvVersion.text = getString(R.string.version_prefix, versionName) + binding.root.setOnClickListener { hideKeyboard() } binding.btnSave.setOnClickListener { @@ -54,6 +62,28 @@ class SettingsActivity : AppCompatActivity() { } setupRestartButton() + setupSpecialTaskSwitch() + } + + private fun setupSpecialTaskSwitch() { + val isSpecialTaskMode = prefs.getBoolean("special_task_mode", false) + binding.switchSpecialTask.isChecked = isSpecialTaskMode + updateStatusIndicator(isSpecialTaskMode) + + binding.switchSpecialTask.setOnCheckedChangeListener { _, isChecked -> + prefs.edit().putBoolean("special_task_mode", isChecked).apply() + updateStatusIndicator(isChecked) + } + } + + private fun updateStatusIndicator(isSpecialTaskMode: Boolean) { + val indicatorColor = if (isSpecialTaskMode) { + ContextCompat.getColor(this, android.R.color.holo_red_dark) + } else { + ContextCompat.getColor(this, android.R.color.holo_green_dark) + } + val indicatorDrawable = binding.statusIndicator.background as GradientDrawable + indicatorDrawable.setColor(indicatorColor) } private fun setupRestartButton() { diff --git a/app/src/main/res/drawable/custom_progress_bar.xml b/app/src/main/res/drawable/custom_progress_bar.xml index 43e124d..aa706a5 100644 --- a/app/src/main/res/drawable/custom_progress_bar.xml +++ b/app/src/main/res/drawable/custom_progress_bar.xml @@ -3,14 +3,14 @@ - + - + diff --git a/app/src/main/res/drawable/status_indicator.xml b/app/src/main/res/drawable/status_indicator.xml new file mode 100644 index 0000000..2e47e6a --- /dev/null +++ b/app/src/main/res/drawable/status_indicator.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index a8516f1..913f548 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,37 +1,52 @@ - - + - + -