基于Rokid CXR-M SDK的引导式作业辅导系统设计与实现

一、灵光乍现

"这题怎么又不会?上次不是讲过吗?"

周末的下午,邻居家传来熟悉的"辅导作业"声。这场景太常见了------家长越讲越急,孩子越听越懵。

问题的本质是什么?经过观察,我发现三个关键矛盾:

帮助与依赖的博弈:直接告诉答案,孩子秒懂但没学会;不给答案,孩子发呆半小时。

情绪与理性的拉扯:面对"简单题目做错",理性说要耐心,情绪却先爆发。

陪伴与独立的平衡:陪太紧孩子依赖,陪太松孩子走神。

正是在思考这个问题时,我想到了 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 的工作流程:

  1. 场景控制controlScene 告诉眼镜"我要用提词器场景了"
  1. 流式传输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

参考资源

  • 维果茨基《思维与语言》
相关推荐
代码搬运媛2 小时前
Generator 迭代器协议 & co 库底层原理+实战
前端
前端拿破轮2 小时前
从0到1搭建个人网站(三):用 Cloudflare R2 + PicGo 搭建高速图床
前端·后端·面试
功能啥都不会2 小时前
PM2 使用指南 - 踩坑记录
前端
HelloReader2 小时前
React 中 useState、useEffect、useRef 的区别与使用场景详解,终于有人讲明白了
前端
兆子龙2 小时前
CSS 里的「if」:@media、@supports 与即将到来的 @when/@else
前端
踩着两条虫2 小时前
AI 智能体如何重构开发工作流
前端·人工智能·低代码
代码老中医2 小时前
逃离"Div汤":2026年,当AI写了75%的代码,前端开发者还剩什么?
前端
左夕2 小时前
最基础的类型检测工具——typeof, instanceof
前端·javascript
yuki_uix2 小时前
递归:别再"展开脑补"了,学会"信任"才是关键
前端·javascript