一、灵光乍现
"这题怎么又不会?上次不是讲过吗?"
周末的下午,邻居家传来熟悉的"辅导作业"声。这场景太常见了------家长越讲越急,孩子越听越懵。
问题的本质是什么?经过观察,我发现三个关键矛盾:
帮助与依赖的博弈:直接告诉答案,孩子秒懂但没学会;不给答案,孩子发呆半小时。
情绪与理性的拉扯:面对"简单题目做错",理性说要耐心,情绪却先爆发。
陪伴与独立的平衡:陪太紧孩子依赖,陪太松孩子走神。
正是在思考这个问题时,我想到了 Rokid AR 眼镜------如果让眼镜来做这个"中间人",会怎样?
二、设计哲学:不是"答案机",而是"引路人"
核心定位:它不是"告诉答案"的工具,而是"引导思考"的桥梁。
渐进式提示系统
传统辅导APP:拍照 → 识别 → 出答案。快速,但孩子学到了什么?
我设计了"三段式"渐进提示:

每一层独立触发,家长完全掌控节奏。
角色分离架构
| 设备 | 使用者 | 功能定位 |
|---|---|---|
| AR眼镜 | 孩子 | 只看到提示,专注思考 |
| 手机APP | 家长 | 控制进度,观察反应 |
三、技术架构:CXR-M SDK 的正确打开方式
3.1 为什么选择 CXR-M SDK?
Rokid 提供了多种SDK选项,为什么最终选择 CXR-M?这要从它的核心特性说起:
提词器场景(Word Tips) 是 CXR-M SDK 的杀手级功能。它允许手机端实时发送文本内容到眼镜端,并以提词器的形式呈现。这正是我们需要的------将提示内容"推送"到孩子眼前。
更重要的是,SDK 的 sendStream API 支持动态更新内容,这让"逐步推进提示"成为可能。
3.2 项目依赖配置
首先,在 settings.gradle.kts 中添加 Rokid Maven 仓库:
scss
dependencyResolutionManagement {
repositories {
maven { url = uri("https://maven.rokid.com/repository/maven-public/") }
google()
mavenCentral()
}
}

然后在 app/build.gradle.kts 中引入 SDK:
scss
dependencies {
implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2")
// 协程支持,用于异步操作
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// Lifecycle 组件,管理UI状态
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
}

3.3 权限声明
眼镜通过蓝牙连接手机,需要在 AndroidManifest.xml 中声明蓝牙权限:
xml
<!-- 蓝牙基础权限 -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- Android 12+ 蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
注意:Android 12 引入了新的蓝牙权限模型,需要同时处理新旧两种权限声明方式。
四、核心代码实现:从连接到推送
4.1 SDK 封装层设计
为了解耦业务逻辑与SDK调用,我设计了一个单例管理类 RokidGlassesManager:

kotlin
package com.rokid.homework.sdk
import com.rokid.cxr.m.CxrApi
import com.rokid.cxr.m.callback.SendStatusCallback
import com.rokid.cxr.m.enums.CxrSendErrorCode
import com.rokid.cxr.m.utils.ValueUtil
/**
* Rokid眼镜连接管理器
* 封装CXR-M SDK的核心API,提供简化的调用接口
*/
object RokidGlassesManager {
private val cxrApi: CxrApi = CxrApi.getInstance()
/**
* 检查眼镜连接状态
*/
val isConnected: Boolean
get() = cxrApi.isBluetoothConnected
/**
* 发送提示内容到眼镜
*
* @param text 要显示的提示文本
* @param callback 发送结果回调
*/
fun sendHint(text: String, callback: SendCallback?) {
// 前置检查:眼镜是否已连接
if (!isConnected) {
callback?.onFailed("眼镜未连接,请检查蓝牙")
return
}
// 第一步:激活提词器场景
cxrApi.controlScene(
ValueUtil.CxrSceneType.WORD_TIPS,
true, // 开启场景
null
)
// 第二步:发送文本流
cxrApi.sendStream(
type = ValueUtil.CxrStreamType.WORD_TIPS,
stream = text.toByteArray(Charsets.UTF_8),
fileName = "homework_hint.txt",
cb = object : SendStatusCallback() {
override fun onSendSucceed() {
callback?.onSuccess()
}
override fun onSendFailed(code: CxrSendErrorCode?) {
callback?.onFailed("发送失败: ${code?.name}")
}
}
)
}
/**
* 发送回调接口
*/
interface SendCallback {
fun onSuccess()
fun onFailed(message: String?)
}
}
这段代码的核心在于理解 CXR-M SDK 的工作流程:
- 场景控制 :
controlScene告诉眼镜"我要用提词器场景了"
- 流式传输 :
sendStream将实际内容推送到眼镜
4.2 分步提示的构建逻辑
提示内容不是简单的一行文本,而是结构化的信息。我设计了 HintBuilder 来组装提示内容:
scss
package com.rokid.homework.core
import com.rokid.homework.data.Question
import com.rokid.homework.data.Subject
/**
* 提示内容构建器
* 负责将题目和提示组装成眼镜端显示的格式
*/
object HintBuilder {
/**
* 构建当前步骤的提示文本
*
* @param question 当前题目
* @param stepIndex 当前步骤索引 (-1=仅题目, 0~n-1=提示步骤, n=答案)
*/
fun build(question: Question, stepIndex: Int): String = buildString {
// 标题区域:科目信息
appendLine("📚 ${question.subject.displayName}")
appendLine()
// 题目区域:始终显示
appendLine("══════ 题目 ══════")
appendLine()
appendLine(wrapText(question.content, 24))
appendLine()
// 提示区域:根据步骤动态显示
if (stepIndex >= 0 && question.hints.isNotEmpty()) {
val currentHintCount = minOf(stepIndex + 1, question.hints.size)
appendLine("══════ 提示 $currentHintCount/${question.hints.size} ══════")
appendLine()
// 逐步累加显示历史提示
for (i in 0 until currentHintCount) {
appendLine("▸ ${question.hints[i]}")
}
appendLine()
}
// 答案区域:所有提示显示完后才能看
if (stepIndex >= question.hints.size) {
appendLine("══════ 参考答案 ══════")
appendLine()
question.answer.forEach { ans ->
appendLine("✓ $ans")
}
}
}
/**
* 文本换行处理
* 眼镜屏幕宽度有限,需要手动控制每行字符数
*/
private fun wrapText(text: String, maxLineLength: Int): String {
return text.chunked(maxLineLength).joinToString("\n")
}
}
这里有个细节值得注意:stepIndex 的设计。
-1:只显示题目,不显示任何提示(初始状态)
0:显示第1个提示
1:显示第1+2个提示(提示是累加显示的)
- ...
hints.size:显示完整答案
这种"累加式"显示的设计理念是:让孩子看到完整的思考过程,而不是每次只看到一个孤立的提示。
4.3 数据模型:题目的结构化表达
kotlin
package com.rokid.homework.data
/**
* 学科枚举
*/
enum class Subject(val displayName: String, val icon: String) {
MATH("数学", "🔢"),
CHINESE("语文", "📖"),
ENGLISH("英语", "🔤"),
PHYSICS("物理", "⚡"),
CHEMISTRY("化学", "🧪")
}
/**
* 题目数据模型
*
* @param id 唯一标识
* @param subject 所属学科
* @param content 题目内容
* @param hints 分步提示列表(按渐进顺序)
* @param answer 最终答案
* @param difficulty 难度等级 1-5
*/
data class Question(
val id: Int,
val subject: Subject,
val content: String,
val hints: List<String>,
val answer: List<String>,
val difficulty: Int = 1
) {
/**
* 获取提示总步数
*/
val totalSteps: Int
get() = hints.size + 1 // +1 是答案步骤
/**
* 判断指定步骤是否为答案步骤
*/
fun isAnswerStep(stepIndex: Int): Boolean = stepIndex >= hints.size
}
4.4 预设题库
为了演示,我准备了几个典型题目:
less
package com.rokid.homework.data
import Subject.*
object QuestionBank {
val allQuestions = listOf(
// ═══════ 数学题 ═══════
Question(
id = 1,
subject = MATH,
content = "解方程:2x + 5 = 15,求 x 的值",
hints = listOf(
"先把常数项 5 移到等号右边",
"移项后变成:2x = 15 - 5 = 10",
"然后两边同时除以 2"
),
answer = listOf("x = 5"),
difficulty = 2
),
Question(
id = 2,
subject = MATH,
content = "一个长方形的周长是24cm,长是宽的2倍,求面积",
hints = listOf(
"设宽为 x,则长为 2x",
"周长公式:2 × (长 + 宽) = 24",
"代入得:2 × (2x + x) = 24,解出 x",
"面积 = 长 × 宽"
),
answer = listOf("宽 = 4cm,长 = 8cm", "面积 = 32cm²"),
difficulty = 3
),
// ═══════ 语文题 ═══════
Question(
id = 3,
subject = CHINESE,
content = "默写《静夜思》并说明表达了什么情感",
hints = listOf(
"作者是唐代诗人李白",
"前两句:床前明月光,疑是地上霜",
"后两句:举头望明月,低头思故乡",
"从「思故乡」可以推断情感"
),
answer = listOf(
"床前明月光,疑是地上霜",
"举头望明月,低头思故乡",
"表达了诗人漂泊异乡的思乡之情"
),
difficulty = 2
),
// ═══════ 英语题 ═══════
Question(
id = 4,
subject = ENGLISH,
content = "翻译句子:The red apples on the table are very sweet",
hints = listOf(
"red = 红色的,apples = 苹果",
"on the table = 在桌子上",
"very sweet = 非常甜",
"注意中文语序调整"
),
answer = listOf("桌子上的红苹果非常甜"),
difficulty = 1
),
// ═══════ 物理题 ═══════
Question(
id = 5,
subject = PHYSICS,
content = "一个物体从静止开始自由落体,3秒后速度是多少?(g=10m/s²)",
hints = listOf(
"自由落体初速度 v₀ = 0",
"速度公式:v = v₀ + gt",
"代入 g = 10,t = 3"
),
answer = listOf("v = 0 + 10 × 3 = 30m/s"),
difficulty = 2
)
)
/**
* 按学科筛选题目
*/
fun getBySubject(subject: Subject): List<Question> =
allQuestions.filter { it.subject == subject }
/**
* 获取指定ID的题目
*/
fun getById(id: Int): Question? =
allQuestions.find { it.id == id }
}
五、状态管理:让提示"听话地"推进
5.1 ViewModel 的设计
kotlin
package com.rokid.homework.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rokid.homework.data.Question
import com.rokid.homework.data.QuestionBank
import com.rokid.homework.data.Subject
import com.rokid.homework.sdk.RokidGlassesManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class HomeworkViewModel : ViewModel() {
// ═══════ 状态定义 ═══════
private val _currentQuestion = MutableStateFlow<Question?>(null)
val currentQuestion: StateFlow<Question?> = _currentQuestion
private val _currentStep = MutableStateFlow(-1)
val currentStep: StateFlow<Int> = _currentStep
private val _isGlassesConnected = MutableStateFlow(false)
val isGlassesConnected: StateFlow<Boolean> = _isGlassesConnected
private val _sendStatus = MutableStateFlow<SendStatus>(SendStatus.Idle)
val sendStatus: StateFlow<SendStatus> = _sendStatus
// ═══════ 公开方法 ═══════
/**
* 选择题目
*/
fun selectQuestion(question: Question) {
_currentQuestion.value = question
_currentStep.value = -1 // 重置到初始状态
sendCurrentHint()
}
/**
* 推进到下一步提示
*/
fun nextStep() {
val question = _currentQuestion.value ?: return
if (_currentStep.value < question.hints.size) {
_currentStep.value++
sendCurrentHint()
}
}
/**
* 返回上一步提示
*/
fun previousStep() {
if (_currentStep.value >= 0) {
_currentStep.value--
sendCurrentHint()
}
}
/**
* 直接显示答案
*/
fun showAnswer() {
val question = _currentQuestion.value ?: return
_currentStep.value = question.hints.size
sendCurrentHint()
}
/**
* 检查眼镜连接状态
*/
fun checkConnection() {
_isGlassesConnected.value = RokidGlassesManager.isConnected
}
// ═══════ 私有方法 ═══════
private fun sendCurrentHint() {
val question = _currentQuestion.value ?: return
val hintText = HintBuilder.build(question, _currentStep.value)
_sendStatus.value = SendStatus.Sending
RokidGlassesManager.sendHint(hintText, object : RokidGlassesManager.SendCallback {
override fun onSuccess() {
_sendStatus.value = SendStatus.Success
}
override fun onFailed(message: String?) {
_sendStatus.value = SendStatus.Failed(message ?: "未知错误")
}
})
}
}
// 发送状态密封类
sealed class SendStatus {
object Idle : SendStatus()
object Sending : SendStatus()
object Success : SendStatus()
data class Failed(val message: String) : SendStatus()
}
5.2 状态流转图

六、开发中的"坑"与填坑实录
6.1 坑一:眼镜端文字截断
现象:题目内容稍长,眼镜端显示就出现截断,后面看不到。
原因分析 :眼镜屏幕分辨率有限,而 sendStream 发送的文本没有自动换行处理。
解决方案:在发送前预处理文本,强制每20-24个字符换行:
kotlin
private fun wrapText(text: String, maxLineLength: Int = 24): String {
val result = StringBuilder()
var currentLineLength = 0
for (char in text) {
result.append(char)
currentLineLength++
// 中文字符算2个宽度
val charWidth = if (char.code > 127) 2 else 1
if (currentLineLength + charWidth >= maxLineLength) {
result.append('\n')
currentLineLength = 0
}
}
return result.toString()
}
6.2 坑二:蓝牙连接状态不稳定
现象 :明明眼镜连着手机蓝牙,但 isBluetoothConnected 有时返回 false。
原因分析:CXR-M SDK 需要眼镜和手机APP建立专用连接,单纯的系统蓝牙配对不够。
解决方案:在应用启动时主动调用连接检查,并添加重试机制:
kotlin
fun ensureConnection(retryCount: Int = 3): Boolean {
repeat(retryCount) {
if (cxrApi.isBluetoothConnected) return true
Thread.sleep(500) // 等待连接建立
}
return cxrApi.isBluetoothConnected
}
6.3 坑三:发送失败的错误码含义不清
现象 :onSendFailed 返回的错误码如 CODE_STREAM_ERROR 太笼统,难以定位问题。
解决方案:建立错误码映射表,给用户友好提示:
kotlin
private val errorMessages = mapOf(
CxrSendErrorCode.CODE_DEVICE_NOT_CONNECTED to "眼镜未连接",
CxrSendErrorCode.CODE_STREAM_ERROR to "数据传输失败,请重试",
CxrSendErrorCode.CODE_TIMEOUT to "发送超时,请检查连接"
)
fun getErrorMessage(code: CxrSendErrorCode?): String {
return errorMessages[code] ?: "发送失败: ${code?.name}"
}
七、实际使用场景演示
场景一:数学方程题
ini
家长选择题目:解方程 2x + 5 = 15
眼镜端初始显示:
┌─────────────────────────┐
│ 📚 数学 │
│ │
│ ══════ 题目 ══════ │
│ │
│ 解方程:2x + 5 = 15 │
│ 求 x 的值 │
└─────────────────────────┘
家长点击"下一步"后:
┌─────────────────────────┐
│ 📚 数学 │
│ │
│ ══════ 题目 ══════ │
│ 解方程:2x + 5 = 15 │
│ │
│ ══════ 提示 1/3 ══════ │
│ │
│ ▸ 先把常数项5移到等号右边 │
└─────────────────────────┘
场景二:古诗默写
家长选择题目:默写《静夜思》
渐进提示过程:
提示1:作者是唐代诗人李白
提示2:前两句:床前明月光,疑是地上霜
提示3:后两句:举头望明月,低头思故乡
答案:完整诗句 + 情感分析
孩子可以先尝试回忆,实在想不起来再看第一层提示,还不行再看第二层。每一步都是"拉一把"而不是"抱起来"。
八、教育场景的深度思考
8.1 为什么 AR 眼镜比手机更适合?
| 维度 | 手机APP | AR眼镜 |
|---|---|---|
| 注意力 | 通知弹窗、其他APP干扰 | 沉浸式,只有当前内容 |
| 交互方式 | 触摸屏幕,孩子可能乱点 | 家长手机远程控制 |
| 视角 | 低头看屏幕,与书本分离 | 抬头可见,与学习环境融合 |
| 心理感受 | "又在玩手机" | "用科技学习" |
8.2 "引导式"vs"直接式"的教育学依据
维果茨基的"最近发展区"理论指出:学习者独立解决问题的水平,与在成人指导下解决问题的水平之间存在差距。好的教育应该在这个"区"内提供支架(Scaffolding) 。
分步提示系统本质上就是数字化支架:
- 太简单的问题 → 不需要支架
- 太难的问题 → 支架也够不着
- 刚好在"区"内的问题 → 支架让孩子自己爬上去
8.3 可能的扩展方向
AI 能力接入:接入大模型,实现:
- 拍照识别手写题目
- 自动生成分步提示
- 根据孩子水平动态调整难度
学习数据积累:
- 记录哪些题目需要多次提示
- 识别薄弱知识点
- 生成个性化练习建议
多设备协同:
- 平板显示完整解析
- 眼镜只显示关键引导
- 手机作为家长控制台
九、写在最后
这个项目的出发点很小------只是想让邻居家少一点"辅导作业的争吵"。但深入做下去,发现它触及了一个本质问题:技术如何在教育中扮演正确的角色?
不是用AI代替老师,不是用APP代替家长,而是用技术放大"好的教育方式"的效果------耐心、渐进、引导。
AR眼镜恰好提供了一种可能:让孩子在需要帮助的时候,能够"悄悄地"获得提示,而不是在众目睽睽下被"纠正"。这份小小的尊严,或许正是独立思考萌芽的土壤。
代码量不大,理念却很重。希望这个小小的尝试,能给教育科技领域的探索者们一点启发。
项目结构:
bash
HomeworkHelper/
├── app/
│ └── src/main/
│ ├── java/com/rokid/homework/
│ │ ├── sdk/ # SDK封装层
│ │ ├── data/ # 数据模型
│ │ ├── core/ # 核心业务逻辑
│ │ └── ui/ # 界面与ViewModel
│ └── res/
└── 征文.md
参考资源:
- 维果茨基《思维与语言》