feat: 添加表情动画视图和MQTT连接支持
- 新增 AnimatedEmojiView 实现机器人表情动画显示 - 集成 MQTT 客户端库并实现 MqttManager 管理连接 - 添加机器人语音播报功能并同步表情状态 - 移除 SettingsActivity 中的日志显示相关代码 - 更新依赖项和权限配置以支持新功能
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,3 +13,4 @@
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
*.log
|
||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
|
||||
@@ -43,6 +43,9 @@ dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidx.emoji2.views)
|
||||
implementation("org.eclipse.paho:org.eclipse.paho.android.service:1.1.1")
|
||||
implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -17,23 +16,19 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Lzwcaiterminaltemi"
|
||||
tools:targetApi="31">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".SettingsActivity" />
|
||||
|
||||
<activity android:name=".SettingsActivity"
|
||||
android:exported="false"
|
||||
android:label="Settings"/>
|
||||
<service android:name="org.eclipse.paho.android.service.MqttService" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.robotemi.sdk.metadata.SKILL"
|
||||
android:value="@string/app_name" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.example.lzwcai_terminal_temi
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.animation.LinearInterpolator
|
||||
|
||||
class AnimatedEmojiView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs) {
|
||||
|
||||
private val facePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.YELLOW
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val eyePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.BLACK
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val mouthPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.BLACK
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 15f
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
}
|
||||
|
||||
private var mouthPath = RectF()
|
||||
private var mouthAnimator: ValueAnimator? = null
|
||||
private var noddingAnimator: ValueAnimator? = null
|
||||
private var mouthOpenRatio = 0.1f
|
||||
private var noddingOffset = 0f
|
||||
|
||||
enum class Expression { SMILE, NEUTRAL, TALKING, HAPPY, SAD, WINK }
|
||||
var currentExpression = Expression.SMILE
|
||||
set(value) {
|
||||
field = value
|
||||
if (value == Expression.TALKING) {
|
||||
startTalkingAnimation()
|
||||
startNoddingAnimation()
|
||||
} else {
|
||||
stopTalkingAnimation()
|
||||
stopNoddingAnimation()
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
canvas.save()
|
||||
canvas.translate(0f, noddingOffset) // 应用点头动画的偏移
|
||||
|
||||
val centerX = width / 2f
|
||||
val centerY = height / 2f
|
||||
val radius = (minOf(width, height) / 2f) * 0.8f
|
||||
|
||||
// 1. 画脸 (已移除)
|
||||
// canvas.drawCircle(centerX, centerY, radius, facePaint)
|
||||
|
||||
// 2. 画眼睛
|
||||
val eyeRadius = radius * 0.1f
|
||||
val eyeOffsetX = radius * 0.4f
|
||||
val eyeOffsetY = radius * 0.3f
|
||||
|
||||
if (currentExpression == Expression.WINK) {
|
||||
val closedEyeWidth = eyeRadius * 2.5f
|
||||
mouthPaint.style = Paint.Style.STROKE
|
||||
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)
|
||||
}
|
||||
|
||||
// 3. 画嘴巴
|
||||
mouthPaint.style = Paint.Style.STROKE
|
||||
val mouthWidth = radius * 0.6f
|
||||
val mouthHeight = radius * 0.4f
|
||||
val mouthLeft = centerX - mouthWidth / 2
|
||||
val mouthTop = centerY + radius * 0.1f
|
||||
|
||||
mouthPath.set(mouthLeft, mouthTop, mouthLeft + mouthWidth, mouthTop + mouthHeight)
|
||||
|
||||
when (currentExpression) {
|
||||
Expression.SMILE -> canvas.drawArc(mouthPath, 20f, 140f, false, mouthPaint)
|
||||
Expression.HAPPY -> {
|
||||
val happyMouthPath = RectF(mouthLeft, mouthTop - radius * 0.1f, mouthLeft + mouthWidth, mouthTop + mouthHeight)
|
||||
canvas.drawArc(happyMouthPath, 0f, 180f, false, mouthPaint)
|
||||
}
|
||||
Expression.SAD -> 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 -> {
|
||||
mouthPaint.style = Paint.Style.FILL
|
||||
val dynamicMouthHeight = mouthHeight * mouthOpenRatio
|
||||
val dynamicMouthTop = centerY + radius * 0.2f - dynamicMouthHeight / 2
|
||||
mouthPath.set(mouthLeft, dynamicMouthTop, mouthLeft + mouthWidth, dynamicMouthTop + dynamicMouthHeight)
|
||||
canvas.drawOval(mouthPath, mouthPaint)
|
||||
}
|
||||
}
|
||||
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
private fun startTalkingAnimation() {
|
||||
if (mouthAnimator?.isRunning == true) return
|
||||
mouthAnimator = ValueAnimator.ofFloat(0.1f, 1.0f, 0.1f).apply {
|
||||
duration = 600
|
||||
interpolator = LinearInterpolator()
|
||||
repeatCount = ValueAnimator.INFINITE
|
||||
addUpdateListener { animation ->
|
||||
mouthOpenRatio = animation.animatedValue as Float
|
||||
invalidate()
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopTalkingAnimation() {
|
||||
mouthAnimator?.cancel()
|
||||
mouthOpenRatio = 0.1f
|
||||
}
|
||||
|
||||
private fun startNoddingAnimation() {
|
||||
if (noddingAnimator?.isRunning == true) return
|
||||
noddingAnimator = ValueAnimator.ofFloat(-15f, 15f, -15f).apply {
|
||||
duration = 800
|
||||
interpolator = LinearInterpolator()
|
||||
repeatCount = ValueAnimator.INFINITE
|
||||
addUpdateListener { animation ->
|
||||
noddingOffset = animation.animatedValue as Float
|
||||
invalidate()
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopNoddingAnimation() {
|
||||
noddingAnimator?.cancel()
|
||||
noddingOffset = 0f
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
stopTalkingAnimation()
|
||||
stopNoddingAnimation()
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,55 @@
|
||||
package com.example.lzwcai_terminal_temi
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
|
||||
object LogManager {
|
||||
private val _logs = MutableLiveData<String>("")
|
||||
val logs: LiveData<String> = _logs
|
||||
private val logBuffer = StringBuffer()
|
||||
private var isReading = false
|
||||
private val currentPid = android.os.Process.myPid().toString()
|
||||
private var logcatJob: Job? = null
|
||||
private var logcatProcess: Process? = null
|
||||
private val logListeners = mutableListOf<(String) -> Unit>()
|
||||
|
||||
fun startLogcatListener() {
|
||||
if (isReading) return
|
||||
isReading = true
|
||||
|
||||
Thread {
|
||||
if (logcatJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
logcatJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
// 清除之前的日志缓存
|
||||
Runtime.getRuntime().exec("logcat -c")
|
||||
|
||||
// 开始读取日志,过滤当前进程ID
|
||||
val process = Runtime.getRuntime().exec("logcat -v threadtime")
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
|
||||
logcatProcess = ProcessBuilder("logcat", "-v", "time").start()
|
||||
val reader = BufferedReader(InputStreamReader(logcatProcess!!.inputStream))
|
||||
var line: String?
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
line?.let {
|
||||
if (it.contains(currentPid)) {
|
||||
// 简单的过滤,只显示包含当前PID的行
|
||||
// 可以根据需要进一步处理日志格式
|
||||
updateLog(it)
|
||||
}
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
updateLog(line!!)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("LogManager", "Error reading logcat", e)
|
||||
updateLog("Error reading logs: ${e.message}")
|
||||
Log.e("LogManager", "Error starting logcat listener", e)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun updateLog(msg: String) {
|
||||
// 限制日志缓冲区大小,避免内存溢出
|
||||
if (logBuffer.length > 50000) {
|
||||
logBuffer.delete(0, 10000)
|
||||
}
|
||||
logBuffer.append("$msg\n")
|
||||
_logs.postValue(logBuffer.toString())
|
||||
}
|
||||
|
||||
fun clearLogs() {
|
||||
logBuffer.setLength(0)
|
||||
_logs.postValue("")
|
||||
// 也可以尝试清除系统日志缓存,但通常需要权限或只能清除应用自己的
|
||||
Thread {
|
||||
try {
|
||||
Runtime.getRuntime().exec("logcat -c")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}.start()
|
||||
fun stopLogcatListener() {
|
||||
logcatJob?.cancel()
|
||||
logcatProcess?.destroy()
|
||||
logcatProcess = null
|
||||
logcatJob = null
|
||||
Log.i("LogManager", "Logcat listener stopped.")
|
||||
}
|
||||
|
||||
fun addLogListener(listener: (String) -> Unit) {
|
||||
logListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeLogListener(listener: (String) -> Unit) {
|
||||
logListeners.remove(listener)
|
||||
}
|
||||
|
||||
private fun updateLog(logLine: String) {
|
||||
logListeners.forEach { it(logLine) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,23 @@ package com.example.lzwcai_terminal_temi
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.example.lzwcai_terminal_temi.databinding.ActivityMainBinding
|
||||
import com.robotemi.sdk.Robot
|
||||
import com.robotemi.sdk.TtsRequest
|
||||
import com.robotemi.sdk.Robot.TtsListener
|
||||
import com.robotemi.sdk.listeners.OnRobotReadyListener
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private lateinit var robot: Robot
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private var mqttManager: MqttManager? = null
|
||||
private lateinit var prefs: SharedPreferences
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -21,23 +27,104 @@ class MainActivity : AppCompatActivity() {
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
// 启动日志监听
|
||||
LogManager.startLogcatListener()
|
||||
|
||||
// 隐藏软键盘
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
|
||||
|
||||
// 获取 Robot 实例
|
||||
robot = Robot.getInstance()
|
||||
|
||||
// 设置按钮点击事件
|
||||
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
|
||||
binding.btnSettings.setOnClickListener {
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
}
|
||||
|
||||
// 读取配置的 IP 并记录日志
|
||||
val prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
val ip = prefs.getString("network_ip", "未设置")
|
||||
Log.i("MainActivity", getString(R.string.log_init, ip))
|
||||
binding.btnRandomExpression.setOnClickListener {
|
||||
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.values().random()
|
||||
}
|
||||
|
||||
binding.btnSpeak.setOnClickListener {
|
||||
speakLongSentence()
|
||||
}
|
||||
|
||||
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
|
||||
|
||||
updateMqttConnection()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
robot.addOnRobotReadyListener(this)
|
||||
robot.addTtsListener(this)
|
||||
prefs.registerOnSharedPreferenceChangeListener(this)
|
||||
mqttManager?.connect()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
robot.removeOnRobotReadyListener(this)
|
||||
robot.removeTtsListener(this)
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
||||
mqttManager?.disconnect()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// 确保在应用销毁时彻底释放资源
|
||||
mqttManager?.disconnect()
|
||||
LogManager.stopLogcatListener()
|
||||
Log.i("MainActivity", "All resources released on destroy.")
|
||||
}
|
||||
|
||||
override fun onRobotReady(isReady: Boolean) {
|
||||
if (isReady) {
|
||||
Log.i("MainActivity", "Robot is ready!")
|
||||
}
|
||||
}
|
||||
|
||||
private fun speakLongSentence() {
|
||||
val longSentence = "你好,我是一个智能机器人。我很高兴能为您服务。现在,我将为您展示我的各种表情和语音能力。希望您喜欢!"
|
||||
val ttsRequest = TtsRequest.create(longSentence, false, language = TtsRequest.Language.ZH_CN)
|
||||
robot.speak(ttsRequest)
|
||||
}
|
||||
|
||||
override fun onTtsStatusChanged(ttsRequest: TtsRequest) {
|
||||
when (ttsRequest.status) {
|
||||
TtsRequest.Status.STARTED -> {
|
||||
Log.i("MainActivity", "TTS started: ${ttsRequest.speech}")
|
||||
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.TALKING
|
||||
}
|
||||
TtsRequest.Status.COMPLETED -> {
|
||||
Log.i("MainActivity", "TTS completed: ${ttsRequest.speech}")
|
||||
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
|
||||
}
|
||||
TtsRequest.Status.CANCELED -> {
|
||||
Log.w("MainActivity", "TTS canceled: ${ttsRequest.speech}")
|
||||
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
|
||||
}
|
||||
TtsRequest.Status.ERROR -> {
|
||||
Log.e("MainActivity", "TTS error: ${ttsRequest.speech}")
|
||||
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SAD
|
||||
}
|
||||
else -> { /* PENDING, PROCESSING, NOT_ALLOWED */ }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
if (key == "network_ip") {
|
||||
Log.i("MainActivity", "IP address changed, re-initializing MQTT connection.")
|
||||
updateMqttConnection()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMqttConnection() {
|
||||
mqttManager?.disconnect()
|
||||
val ip = prefs.getString("network_ip", null)
|
||||
if (!ip.isNullOrEmpty()) {
|
||||
mqttManager = MqttManager(this, ip)
|
||||
mqttManager?.connect()
|
||||
Log.i("MainActivity", "MQTT Manager updated with new IP: $ip")
|
||||
} else {
|
||||
mqttManager = null
|
||||
Log.w("MainActivity", "MQTT Manager disabled: IP address is not set.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.example.lzwcai_terminal_temi
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.*
|
||||
import org.eclipse.paho.client.mqttv3.*
|
||||
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
|
||||
|
||||
class MqttManager(private val context: Context, private val serverIp: String) {
|
||||
|
||||
private var mqttClient: MqttClient? = null
|
||||
private val TAG = "MqttManager"
|
||||
private val brokerUri = "tcp://$serverIp:1883"
|
||||
private val clientId = MqttClient.generateClientId()
|
||||
private val job = SupervisorJob()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + job)
|
||||
private var reconnectJob: Job? = null
|
||||
|
||||
init {
|
||||
try {
|
||||
mqttClient = MqttClient(brokerUri, clientId, MemoryPersistence())
|
||||
mqttClient?.setCallback(object : MqttCallbackExtended {
|
||||
override fun connectComplete(reconnect: Boolean, serverURI: String?) {
|
||||
Log.i(TAG, "MQTT connection complete. Reconnect: $reconnect")
|
||||
subscribeTopic("robot/cmd")
|
||||
}
|
||||
|
||||
override fun connectionLost(cause: Throwable?) {
|
||||
Log.e(TAG, "Connection lost: ${cause?.message}")
|
||||
scheduleReconnect()
|
||||
}
|
||||
|
||||
override fun messageArrived(topic: String?, message: MqttMessage?) {
|
||||
val payload = String(message?.payload ?: ByteArray(0))
|
||||
Log.i(TAG, "Message arrived: Topic=$topic, Payload=$payload")
|
||||
}
|
||||
|
||||
override fun deliveryComplete(token: IMqttDeliveryToken?) {
|
||||
// This is not critical for our use case
|
||||
}
|
||||
})
|
||||
} catch (e: MqttException) {
|
||||
Log.e(TAG, "Error creating MQTT client: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun connect() {
|
||||
scope.launch {
|
||||
if (mqttClient?.isConnected == true) {
|
||||
Log.d(TAG, "MQTT client is already connected.")
|
||||
return@launch
|
||||
}
|
||||
Log.i(TAG, "Attempting to connect to MQTT broker at $brokerUri")
|
||||
try {
|
||||
val options = MqttConnectOptions().apply {
|
||||
isAutomaticReconnect = false
|
||||
isCleanSession = true
|
||||
connectionTimeout = 10
|
||||
keepAliveInterval = 60 // 设置心跳间隔为60秒
|
||||
userName = "lzwc"
|
||||
password = "Lzwc@4187.".toCharArray()
|
||||
}
|
||||
mqttClient?.connect(options)
|
||||
} catch (e: MqttException) {
|
||||
Log.e(TAG, "Initial connection failed: ${e.message}")
|
||||
scheduleReconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleReconnect() {
|
||||
if (reconnectJob?.isActive == true) {
|
||||
return // 如果已经在计划重连,则不再创建新的
|
||||
}
|
||||
reconnectJob = scope.launch {
|
||||
Log.d(TAG, "Scheduling reconnect in 5 seconds.")
|
||||
delay(5000L)
|
||||
connect()
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
scope.launch {
|
||||
try {
|
||||
reconnectJob?.cancel() // 取消任何待处理的重连任务
|
||||
if (mqttClient?.isConnected == true) {
|
||||
mqttClient?.disconnect()
|
||||
Log.i(TAG, "Disconnected from MQTT broker.")
|
||||
}
|
||||
} catch (e: MqttException) {
|
||||
Log.e(TAG, "Error disconnecting from MQTT broker: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeTopic(topic: String) {
|
||||
try {
|
||||
if (mqttClient?.isConnected == true) {
|
||||
mqttClient?.subscribe(topic, 1)
|
||||
Log.i(TAG, "Subscribed to topic: $topic")
|
||||
} else {
|
||||
Log.w(TAG, "Cannot subscribe, MQTT client not connected.")
|
||||
}
|
||||
} catch (e: MqttException) {
|
||||
Log.e(TAG, "Error subscribing to topic: $topic", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun publish(topic: String, msg: String, qos: Int = 1, retained: Boolean = false) {
|
||||
scope.launch {
|
||||
if (mqttClient?.isConnected != true) {
|
||||
Log.w(TAG, "MQTT client not connected, cannot publish.")
|
||||
return@launch
|
||||
}
|
||||
try {
|
||||
val message = MqttMessage(msg.toByteArray()).apply {
|
||||
this.qos = qos
|
||||
this.isRetained = retained
|
||||
}
|
||||
mqttClient?.publish(topic, message)
|
||||
} catch (e: MqttException) {
|
||||
Log.e(TAG, "Error publishing message: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,30 +50,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||
finish()
|
||||
}
|
||||
|
||||
binding.btnToggleLogs.setOnClickListener {
|
||||
hideKeyboard()
|
||||
if (binding.tvLog.visibility == View.VISIBLE) {
|
||||
binding.tvLog.visibility = View.GONE
|
||||
binding.btnToggleLogs.text = getString(R.string.btn_show_logs)
|
||||
} else {
|
||||
binding.tvLog.visibility = View.VISIBLE
|
||||
binding.btnToggleLogs.text = getString(R.string.btn_hide_logs)
|
||||
}
|
||||
}
|
||||
|
||||
LogManager.logs.observe(this) { logs ->
|
||||
binding.tvLog.text = logs
|
||||
// 自动滚动到底部
|
||||
binding.tvLog.post {
|
||||
if (binding.tvLog.layout != null) {
|
||||
val scrollAmount = binding.tvLog.layout.getLineTop(binding.tvLog.lineCount) - binding.tvLog.height
|
||||
if (scrollAmount > 0)
|
||||
binding.tvLog.scrollTo(0, scrollAmount)
|
||||
else
|
||||
binding.tvLog.scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
|
||||
@@ -16,4 +16,32 @@
|
||||
android:contentDescription="@string/btn_settings"
|
||||
android:src="@android:drawable/ic_menu_manage" />
|
||||
|
||||
<com.example.lzwcai_terminal_temi.AnimatedEmojiView
|
||||
android:id="@+id/animatedEmojiView"
|
||||
android:layout_width="500dp"
|
||||
android:layout_height="500dp"
|
||||
android:layout_centerInParent="true" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnRandomExpression"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/btn_random_expression" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSpeak"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/btn_speak" />
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -1,3 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">lzwcai-terminal-temi</string>
|
||||
<string name="title_main">主界面</string>
|
||||
@@ -13,4 +14,6 @@
|
||||
<string name="msg_invalid_ip">请输入有效的 IP 地址</string>
|
||||
<string name="log_init">应用启动完成。目标 IP: %1$s</string>
|
||||
<string name="log_placeholder">日志将显示在这里...</string>
|
||||
<string name="btn_random_expression">随机表情</string>
|
||||
<string name="btn_speak">让机器人说话</string>
|
||||
</resources>
|
||||
@@ -7,6 +7,7 @@ junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
appcompat = "1.7.1"
|
||||
material = "1.13.0"
|
||||
emoji2-views = "1.5.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -15,6 +16,8 @@ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "j
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
androidx-emoji2-views = { group = "androidx.emoji2", name = "emoji2-views", version.ref = "emoji2-views" }
|
||||
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user