feat: 添加 Temi 机器人应用基础框架和设置功能
- 启用 ViewBinding 并添加 Temi SDK 依赖 - 新增主界面和设置界面布局及活动 - 实现网络 IP 配置的保存与读取功能 - 添加必要的 Android 权限和清单配置 - 创建项目 README 文档说明运行和预览指南
This commit is contained in:
63
README.md
Normal file
63
README.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Temi 机器人应用开发与预览指南
|
||||||
|
|
||||||
|
本项目是一个基于 Temi SDK 的 Android 应用程序,包含主界面和网络 IP 设置功能。
|
||||||
|
|
||||||
|
## 1. 预览与运行环境
|
||||||
|
|
||||||
|
### 推荐工具:Android Studio
|
||||||
|
由于 Android 应用依赖于复杂的构建系统 (Gradle) 和特定的运行时环境 (Android OS),**无法像网页那样直接通过浏览器或简单的编辑器插件预览**。
|
||||||
|
|
||||||
|
最标准的开发和预览工具是 **Android Studio** (官方 IDE)。
|
||||||
|
|
||||||
|
### Trae 的角色
|
||||||
|
Trae 非常适合编写代码、管理项目结构和进行逻辑开发。但为了查看应用的实际运行效果(UI 和交互),你需要将代码编译成 APK 并安装到设备上,这通常需要 Android Studio 的配合。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 如何在另一台电脑上运行
|
||||||
|
|
||||||
|
### 步骤 1: 安装 Android Studio
|
||||||
|
请访问 [Android Studio 官网](https://developer.android.com/studio) 下载并安装最新版本。
|
||||||
|
|
||||||
|
### 步骤 2: 导入项目
|
||||||
|
1. 打开 Android Studio。
|
||||||
|
2. 选择 **Open** (打开项目)。
|
||||||
|
3. 导航到本项目所在的文件夹 `lzwcai-terminal-temi` 并点击 OK。
|
||||||
|
4. 等待 Android Studio 自动同步 Gradle 依赖(这可能需要几分钟,取决于网络)。
|
||||||
|
|
||||||
|
### 步骤 3: 运行应用 (两种方式)
|
||||||
|
|
||||||
|
#### 方式 A: 连接 Temi 机器人 (强烈推荐)
|
||||||
|
Temi SDK 的很多功能(如语音、导航、跟随)依赖于机器人的硬件传感器。
|
||||||
|
1. 确保 Temi 机器人已开启 **开发者模式** (设置 -> 通用 -> 开发者选项)。
|
||||||
|
2. 使用 USB 线(通常是 USB-A 转 USB-A 或通过背部接口)将电脑连接到 Temi。
|
||||||
|
3. 或者使用 **ADB over Wi-Fi** (确保电脑和 Temi 在同一 Wi-Fi 下)。
|
||||||
|
4. 在 Android Studio 顶部工具栏选择连接的设备,点击 **Run** (绿色三角形按钮)。
|
||||||
|
|
||||||
|
#### 方式 B: 使用 Android 模拟器 (仅限 UI 预览)
|
||||||
|
如果你身边没有机器人,可以使用模拟器预览界面布局,但会报错或功能受限。
|
||||||
|
1. 在 Android Studio 中点击 **Device Manager** 创建一个虚拟设备 (推荐 Pixel 系列,API 30+)。
|
||||||
|
2. 运行模拟器。
|
||||||
|
3. **注意**:由于模拟器没有 Temi 的系统服务,`Robot.getInstance()` 可能会导致崩溃或返回错误。
|
||||||
|
- *提示*:为了在模拟器上查看 UI,你可能需要暂时注释掉 `MainActivity.kt` 中调用 `Robot` 的代码。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 项目结构说明
|
||||||
|
|
||||||
|
- **主界面**: `app/src/main/java/.../MainActivity.kt`
|
||||||
|
- 包含日志显示和跳转设置的按钮。
|
||||||
|
- 初始化 Robot SDK。
|
||||||
|
- **设置界面**: `app/src/main/java/.../SettingsActivity.kt`
|
||||||
|
- 用于设置和保存网络 IP 地址。
|
||||||
|
- **布局文件**:
|
||||||
|
- `app/src/main/res/layout/activity_main.xml` (主页布局)
|
||||||
|
- `app/src/main/res/layout/activity_settings.xml` (设置页布局)
|
||||||
|
|
||||||
|
## 4. 常见问题
|
||||||
|
|
||||||
|
**Q: 为什么在模拟器上闪退?**
|
||||||
|
A: 因为应用启动时会调用 `Robot.getInstance()`,而普通模拟器没有 Temi 的底层服务。
|
||||||
|
|
||||||
|
**Q: Trae 可以装插件预览吗?**
|
||||||
|
A: 目前没有插件能直接在 Trae 内部完美模拟 Android 环境。建议使用 Trae 写代码,配合 Android Studio 或真机进行调试预览。
|
||||||
@@ -33,10 +33,13 @@ android {
|
|||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "11"
|
jvmTarget = "11"
|
||||||
}
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation("com.robotemi:sdk:1.137.1")
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@@ -11,6 +16,24 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Lzwcaiterminaltemi"
|
android:theme="@style/Theme.Lzwcaiterminaltemi"
|
||||||
tools:targetApi="31" />
|
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"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="Settings"/>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.robotemi.sdk.metadata.SKILL"
|
||||||
|
android:value="@string/app_name" />
|
||||||
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,2 +1,70 @@
|
|||||||
package com.example.lzwcai_terminal_temi
|
package com.example.lzwcai_terminal_temi
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.method.ScrollingMovementMethod
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.example.lzwcai_terminal_temi.databinding.ActivityMainBinding
|
||||||
|
import com.robotemi.sdk.Robot
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var robot: Robot
|
||||||
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
|
||||||
|
private val executorService = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
// 隐藏软键盘
|
||||||
|
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
|
||||||
|
|
||||||
|
// 获取 Robot 实例
|
||||||
|
robot = Robot.getInstance()
|
||||||
|
|
||||||
|
// 初始化日志视图
|
||||||
|
binding.tvLog.movementMethod = ScrollingMovementMethod.getInstance()
|
||||||
|
|
||||||
|
// 设置按钮点击事件
|
||||||
|
binding.btnSettings.setOnClickListener {
|
||||||
|
startActivity(Intent(this, SettingsActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 轻量化处理:移除不必要的监听器 ---
|
||||||
|
// 移除了 FaceRecognized, Telepresence, GreetMode 等监听器
|
||||||
|
// 仅保留基础的权限和运动状态监听作为示例
|
||||||
|
|
||||||
|
robot.addOnRequestPermissionResultListener { permission, grantResult, _ ->
|
||||||
|
printLog("Permission: $permission, Result: $grantResult")
|
||||||
|
}
|
||||||
|
|
||||||
|
robot.addOnMovementStatusChangedListener { type, status ->
|
||||||
|
printLog("Movement: $type, Status: $status")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取并显示配置的 IP
|
||||||
|
val prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||||
|
val ip = prefs.getString("network_ip", "未设置")
|
||||||
|
printLog("应用启动完成。目标 IP: $ip")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun printLog(msg: String) {
|
||||||
|
runOnUiThread {
|
||||||
|
binding.tvLog.append("$msg\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
// 建议在销毁时移除监听器,避免内存泄漏 (虽然 Temi SDK 通常会处理,但显式移除是好习惯)
|
||||||
|
// robot.removeOn...
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.example.lzwcai_terminal_temi
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.example.lzwcai_terminal_temi.databinding.ActivitySettingsBinding
|
||||||
|
|
||||||
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivitySettingsBinding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
val prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||||
|
val savedIp = prefs.getString("network_ip", "")
|
||||||
|
binding.etIpAddress.setText(savedIp)
|
||||||
|
|
||||||
|
binding.btnSave.setOnClickListener {
|
||||||
|
val ip = binding.etIpAddress.text.toString().trim()
|
||||||
|
// Here you could add regex validation for IP address if needed
|
||||||
|
if (ip.isNotEmpty()) {
|
||||||
|
prefs.edit().putString("network_ip", ip).apply()
|
||||||
|
Toast.makeText(this, "IP Saved: $ip", Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "Please enter a valid IP", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/src/main/res/layout/activity_main.xml
Normal file
31
app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLog"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:text="Logs will appear here..."
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/btnSettings"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnSettings"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:text="Settings"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
30
app/src/main/res/layout/activity_settings.xml
Normal file
30
app/src/main/res/layout/activity_settings.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Network IP Configuration"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="16dp"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etIpAddress"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Enter IP Address (e.g., 192.168.1.100)"
|
||||||
|
android:inputType="textUri"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnSave"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Save"
|
||||||
|
android:layout_marginTop="16dp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
Reference in New Issue
Block a user