【征文计划】利用 CXR-M SDK 构建 “AR 眼镜 + 手机” 的数学教学系统
一、项目与CXR-M SDK
课堂数学教学中,传统“板书 + 手机拍照”存在视角不统一、步骤关联难、师生互动同步不畅等问题。CXR-M SDK 可实现手机与 Rokid Glasses 稳定连接,调用录音、拍照等能力,接入 YodaOS-Sprite 交互流程,主要是在数学讲解中将“题目与步骤”以第一视角叠加呈现,并同步学生端状态。本文围绕“数学教学系统”,涵盖 SDK 环境搭建、功能落地及避坑指南,帮助开发者将 SDK 能力转化为可用的课堂教学工具。
CXR-M SDK 核心能力与适用范围
CXR-M SDK 是移动端开发工具包(仅 Android 版本),他主要是构建手机与 Rokid Glasses 的控制协同应用,同时支持数据通信,可接入 YodaOS-Sprite 交互流程,调用文件互传、录音、拍照等 Rokid Assist Service 服务。
CXR-M SDK 导入配置指南
以 Kotlin DSL(build.gradle.kts)为例,从三方面说明配置流程:
1.Maven仓库配置
在settings.gradle.kts的dependencyResolutionManagement节点中添加 Rokid Maven 仓库,保留基础仓库:
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
// 新增 Rokid Maven 仓库,用于拉取 CXR-M SDK
maven { url = uri("https://maven.rokid.com/repository/maven-public/") }
google()
mavenCentral()
}
}
2.SDK 与依赖导入
模块级build.gradle.kts中添加核心依赖,设置minSdk=28(最低支持 Android 9.0),并导入配套依赖(版本冲突时优先使用 SDK 版本):
android {
// 其他基础配置(如 compileSdk、buildToolsVersion 等)
defaultConfig {
// 其他配置(如 applicationId、targetSdk 等)
minSdk = 28 // 必须设置为 28 及以上,否则不支持 SDK
}
// 其他配置(如 buildTypes、compileOptions 等)
}
dependencies {
// 其他项目依赖(如 AndroidX、第三方库等)
// 1. CXR-M SDK 核心依赖
implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2")
// 2. SDK 配套依赖(版本冲突时优先使用这些版本)
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.9.3")
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.1.0")
implementation("com.squareup.okio:okio:2.8.0")
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
}
3.权限申请
CXR-M SDK 依赖网络、Wi-Fi、蓝牙(含定位关联权限)等能力,需完成 静态权限声明 和 动态权限申请,否则 SDK 无法正常使用:
静态权限声明
在项目的 AndroidManifest.xml 文件中,需要声明 CXR-M SDK 运行所必需的基础权限集合,含有蓝牙、网络、Wi-Fi 以及定位相关权限,以确保 SDK 功能的正常运行。具体配置代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android
xmlns:tools="http://schemas.android.com/tools">
<!-- 蓝牙相关权限(含 Android S 及以上新增权限) -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- 蓝牙依赖的定位权限 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 网络与 Wi-Fi 权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<application>
<!-- 项目其他配置(如 Activity 注册、Application 声明等) -->
</application>
</manifest>
动态权限申请
对于运行在 Android 6.0(API 23)及以上版本的设备,由于系统引入了运行时权限机制,应用必须在运行时动态申请被归类为"危险权限"的权限组。为了确保 CXR-M SDK 各项功能的正常运行,在初始化 SDK 之前,开发者需要确保所有必需的权限都已获得用户授权。下面展示了如何在 Activity 中实现动态权限申请的完整流程:
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
private const val REQUEST_CODE_PERMISSIONS = 100 // 权限请求码
// 必要权限列表(区分 Android S/API 31 及以上的新增蓝牙权限)
private val REQUIRED_PERMISSIONS = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN
).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
add(Manifest.permission.BLUETOOTH_SCAN)
add(Manifest.permission.BLUETOOTH_CONNECT)
}
}.toTypedArray()
}
// 用于观察权限申请结果的 LiveData
private val isAllPermissionsGranted = MutableLiveData<Boolean?>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 初始化时发起权限申请
isAllPermissionsGranted.postValue(null)
requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
// 观察权限申请结果,决定是否初始化 SDK
isAllPermissionsGranted.observe(this) { granted ->
when (granted) {
true -> {
// 所有权限已授予,可初始化 CXR-M SDK
}
false -> {
// 部分权限被拒绝,需提示用户开启(否则 SDK 不可用)
}
}
}
}
// 接收权限申请结果
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
// 判断所有权限是否均被授予
val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
isAllPermissionsGranted.postValue(allGranted)
}
}
}
二、数学教学场景需求拆解与技术方案
SDK 导入与课堂场景权限优化
SDK 依赖与系统配置优化
依赖配置遵循文档与课堂需求,保留 CXR-M SDK 与基础网络栈;manifestPlaceholders 可设置课堂模式标识,构建配置开启混淆以减少资源占用,满足教室多设备并行的稳定性。
// build.gradle.kts(模块级)
dependencies {
// CXR-M SDK核心依赖(固定版本)
implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2")
// 文档要求的基础依赖(Retrofit、OkHttp等)
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.9.3")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.1.0")
}
android {
defaultConfig {
minSdk = 28 // 遵循SDK要求
targetSdk = 33
// 课堂场景标识(用于区分环境配置)
manifestPlaceholders["classroom_mode"] = "true"
}
// 构建优化:减少课堂多设备压力
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}
课堂场景权限强化
在课堂场景下,对于蓝牙(设备连接)、网络(内容分发)与存储(板书拍照与讲解录音)为必需;动态申请时明确教学用途和影响,拒绝将影响授课流程,需引导开启:
class ClassroomPermissionManager(private val activity: AppCompatActivity) {
// 课堂必需权限:蓝牙(连接)+ 网络(内容同步)+ 存储(素材保存)
private val REQUIRED_PERMISSIONS = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION, // 蓝牙扫描依赖定位
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.WRITE_EXTERNAL_STORAGE // 保存板书/讲解录音
).toTypedArray()
var onPermissionReady: (() -> Unit)? = null
fun requestClassroomPermissions() {
if (isAllPermissionsGranted()) {
onPermissionReady?.invoke()
return
}
ActivityResultContracts.RequestMultiplePermissions().launch(
activity,
REQUIRED_PERMISSIONS
) { resultMap ->
val allGranted = resultMap.values.all { it }
if (!allGranted) {
AlertDialog.Builder(activity)
.setTitle("数学教学必需权限")
.setMessage("蓝牙与存储权限用于设备连接与课堂素材保存,拒绝将影响课堂互动,请前往设置开启")
.setCancelable(false)
.setPositiveButton("去设置") { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", activity.packageName, null)
}
activity.startActivityForResult(intent, 1001)
}
.show()
} else {
onPermissionReady?.invoke()
}
}
}
private fun isAllPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(activity, it) == PackageManager.PERMISSION_GRANTED
}
fun onSettingsReturn() {
if (isAllPermissionsGranted()) {
onPermissionReady?.invoke()
} else {
requestClassroomPermissions()
}
}
}
核心功能实现:课堂场景下的设备协同
设备连接优化:教室多设备稳定连接
初始化 SDK 时设置蓝牙模式与自动重连策略,过滤 Rokid Glasses;连接后同步显示参数(亮度、抗眩光),保证投屏光照下的可视性。
class RokidClassroomDeviceManager(private val context: Context) {
private lateinit var cxrClient: CxrClient
private var targetDeviceId: String? = null
fun initClassroomClient(
appKey: String,
appSecret: String,
accessKey: String,
onInitResult: ((Boolean) -> Unit)
) {
CxrClient.init(
context = context,
appKey = appKey,
appSecret = appSecret,
accessKey = accessKey,
config = CxrConfig.Builder()
.setBluetoothMode(BluetoothMode.NORMAL)
.setReconnectInterval(3000) // 断连后3秒重试
.build()
) { initSuccess ->
if (!initSuccess) {
onInitResult.invoke(false)
return@init
}
cxrClient = CxrClient.getInstance()
onInitResult.invoke(true)
}
}
// 扫描并连接Rokid Glasses(过滤非教学设备)
fun scanAndConnectClassroomDevice(onConnectResult: ((Boolean, String?) -> Unit)) {
cxrClient.scanDevices(
scanMode = ScanMode.NORMAL,
filter = { device -> device.name.contains("Rokid Glasses") },
callback = object : DeviceScanCallback {
override fun onDeviceFound(device: RokidDevice) {
targetDeviceId = device.deviceId
cxrClient.connectDevice(
device = device,
signalListener = { rssi ->
if (rssi < -80) {
Toast.makeText(context, "信号弱,请靠近眼镜或调整位置", Toast.LENGTH_SHORT).show()
}
},
connectCallback = object : ConnectCallback {
override fun onConnected() {
syncClassroomDisplayConfig()
onConnectResult.invoke(true, null)
}
override fun onDisconnected() {
onConnectResult.invoke(false, "设备断开连接,正在重试...")
targetDeviceId?.let { cxrClient.reconnectDevice(it) }
}
override fun onConnectFailed(errorMsg: String) {
onConnectResult.invoke(false, "连接失败:$errorMsg")
}
}
)
}
override fun onScanFailed(errorMsg: String) {
onConnectResult.invoke(false, "扫描设备失败:$errorMsg")
}
}
)
}
// 同步显示配置:投影灯光下提高亮度与抗眩光
private fun syncClassroomDisplayConfig() {
targetDeviceId?.let { deviceId ->
cxrClient.setDeviceConfig(
deviceId = deviceId,
configType = ConfigType.DISPLAY,
configParams = mapOf(
"brightness" to 70,
"antiGlare" to true,
"classroomMode" to true
),
callback = object : ConfigCallback {
override fun onConfigSuccess() {
Log.d("ClassroomConfig", "眼镜显示参数同步成功")
}
override fun onConfigFailed(errorMsg: String) {
Log.e("ClassroomConfig", "显示参数同步失败:$errorMsg")
}
}
)
}
}
// 获取当前眼镜状态(电量、存储、网络)
fun getGlassesStatus(onStatusReady: ((GlassesStatus) -> Unit)) {
targetDeviceId?.let { deviceId ->
cxrClient.getDeviceStatus(
deviceId = deviceId,
statusTypes = listOf(StatusType.BATTERY, StatusType.STORAGE, StatusType.NETWORK),
callback = object : StatusCallback {
override fun onStatusReceived(statusMap: Map<StatusType, Any>) {
val status = GlassesStatus(
battery = statusMap[StatusType.BATTERY] as Int,
storageLeft = statusMap[StatusType.STORAGE] as Long,
networkType = statusMap[StatusType.NETWORK] as String
)
onStatusReady.invoke(status)
}
}
)
}
}
data class GlassesStatus(
val battery: Int,
val storageLeft: Long,
val networkType: String
)
}
核心功能 1:题目下发与 AR 步骤叠加
手机端准备数学题目与解题步骤,解析为 JSON,通过 SDK 的数据飞传至眼镜,并触发 YodaOS-Sprite 场景交互,将“题干 + 关键步骤提示”以 AR 叠加呈现,确保学生第一视角一致。
class MathLessonManager(
private val cxrClient: CxrClient,
private val deviceId: String
) {
// 下发数学题目与步骤
fun deliverMathContent(
title: String, // 题干
steps: List<Step>, // 解题步骤列表
onDelivered: ((Boolean) -> Unit)
) {
// 1. 组装教学内容(题干 + 步骤提示)
val content = MathContent(
title = title,
steps = steps
)
// 2. 转JSON并通过数据飞传同步到眼镜
val contentJson = Gson().toJson(content)
cxrClient.transferData(
deviceId = deviceId,
dataType = DataType.CUSTOM, // 自定义:MATH_CONTENT
data = contentJson.toByteArray(),
callback = object : DataTransferCallback {
override fun onTransferSuccess() {
Log.d("MathSync", "教学内容同步成功")
triggerGlassesTeachingDisplay()
onDelivered.invoke(true)
}
override fun onTransferFailed(errorMsg: String) {
Toast.makeText(context, "内容同步失败:$errorMsg", Toast.LENGTH_SHORT).show()
onDelivered.invoke(false)
}
}
)
}
// 触发眼镜端的数学教学场景显示(AR叠加“题干 + 步骤”)
private fun triggerGlassesTeachingDisplay() {
cxrClient.triggerSceneInteraction(
deviceId = deviceId,
sceneType = SceneType.CUSTOM, // 场景类型:MATH_TEACHING
interactionParams = mapOf("displayMode" to "AR_OVERLAY"),
callback = object : SceneCallback {
override fun onInteractionSuccess() {
Toast.makeText(context, "眼镜AR教学已开启", Toast.LENGTH_SHORT).show()
}
override fun onInteractionFailed(errorMsg: String) {
Log.e("ARDisplay", "AR教学显示失败:$errorMsg")
}
}
)
}
// 数据结构:教学内容与步骤
data class MathContent(
val title: String,
val steps: List<Step>
)
data class Step(
val hint: String, // 步骤提示,如“先因式分解”
val formula: String // 公式,如“(a+b)^2 = a^2 + 2ab + b^2”
)
}
核心功能 2:板书采集与助教协同
板书采集通过眼镜拍照回传手机并关联题目标签;助教协同通过先录音(教师讲解要点)再拍照板书,组装数据包飞传到助教设备,保障课堂资料同步与答疑效率。
class ClassroomAssistManager(
private val cxrClient: CxrClient,
private val deviceId: String
) {
// 1. 板书采集:眼镜端拍照回传,关联题目标签
fun captureBlackboard(tag: String, onCaptured: ((String) -> Unit)) {
cxrClient.takePhoto(
deviceId = deviceId,
photoConfig = PhotoConfig(
beautyMode = false,
resolution = Resolution.HD,
saveToLocal = true
),
callback = object : PhotoCallback {
override fun onPhotoTaken(photoPath: String) {
val msg = "已采集板书【$tag】"
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
onCaptured.invoke(photoPath)
}
override fun onPhotoFailed(errorMsg: String) {
Toast.makeText(context, "板书拍照失败:$errorMsg", Toast.LENGTH_SHORT).show()
}
}
)
}
// 2. 助教协同:录音+拍照,飞传教学资料
fun sendTeachingPack(assistantDeviceId: String, onSent: ((Boolean) -> Unit)) {
cxrClient.startRecording(
deviceId = deviceId,
audioConfig = AudioConfig(
duration = 10,
format = AudioFormat.MP3,
sampleRate = 44100
),
callback = object : RecordingCallback {
override fun onRecordingStopped(audioPath: String) {
takeTeachingPhoto(audioPath, assistantDeviceId, onSent)
}
override fun onRecordingFailed(errorMsg: String) {
Toast.makeText(context, "讲解录音失败:$errorMsg", Toast.LENGTH_SHORT).show()
onSent.invoke(false)
}
}
)
}
// 拍照并飞传资料包
private fun takeTeachingPhoto(
audioPath: String,
assistantDeviceId: String,
onSent: ((Boolean) -> Unit)
) {
cxrClient.takePhoto(
deviceId = deviceId,
photoConfig = PhotoConfig(resolution = Resolution.FHD),
callback = object : PhotoCallback {
override fun onPhotoTaken(photoPath: String) {
val pack = TeachingPack(
photoPath = photoPath,
audioPath = audioPath,
timestamp = System.currentTimeMillis()
)
cxrClient.transferFile(
sourceDeviceId = deviceId,
targetDeviceId = assistantDeviceId,
filePath = Gson().toJson(pack).toByteArray(),
fileType = FileType.CUSTOM, // 自定义:TEACHING_PACK
callback = object : FileTransferCallback {
override fun onTransferSuccess() {
Toast.makeText(context, "教学资料已发送给助教", Toast.LENGTH_SHORT).show()
onSent.invoke(true)
}
override fun onTransferFailed(errorMsg: String) {
Toast.makeText(context, "资料发送失败:$errorMsg", Toast.LENGTH_SHORT).show()
onSent.invoke(false)
}
}
)
}
override fun onPhotoFailed(errorMsg: String) {
Toast.makeText(context, "教学拍照失败:$errorMsg", Toast.LENGTH_SHORT).show()
onSent.invoke(false)
}
}
)
}
data class TeachingPack(
val photoPath: String,
val audioPath: String,
val timestamp: Long
)
}
设备状态监控:课堂续航与存储兜底
用协程定时(1 分钟)检查眼镜电量、存储与网络状态,低电量/低存储/无网络时顶部预警,避免课堂中断。
class ClassroomStatusMonitor(
private val deviceManager: RokidClassroomDeviceManager,
private val context: Context
) {
private val statusCheckInterval = 60000 // 1分钟检查一次状态
fun startStatusMonitor() {
CoroutineScope(Dispatchers.IO).launch {
while (true) {
delay(statusCheckInterval.toLong())
deviceManager.getGlassesStatus { status ->
if (status.battery < 20) {
showWarning("眼镜电量不足20%,请尽快充电")
}
if (status.storageLeft < 100) {
showWarning("眼镜剩余存储不足100MB,建议清理历史板书/录音")
}
if (status.networkType == "NONE") {
showWarning("眼镜无网络连接,内容同步可能延迟")
}
}
}
}
}
private fun showWarning(message: String) {
val toast = Toast.makeText(context, message, Toast.LENGTH_LONG)
toast.setGravity(Gravity.TOP, 0, 100)
toast.show()
}
}
应用场景
场景一:题目讲解第一视角
教师在手机端准备题干与步骤提示,通过数据飞传同步到眼镜,学生以第一视角看到 AR 叠加的“关键步骤与公式”,减少低头翻找资料,提高跟随度。
场景二:板书资料采集与助教协同
眼镜端拍照板书并保存,教师录制关键讲解语音后生成资料包,飞传助教设备用于课后整理与答疑,降低资料碎片化。
场景三:课堂稳定性保障
实时监控电量、存储与网络状态,提前预警,避免因设备状态导致的教学中断。
总结
我们的文章利用 CXR-M SDK 落地“数学教学系统”,结合课堂场景实现题目下发与步骤 AR 叠加、板书采集与助教协同,以及设备状态兜底。整体设计遵循“场景化调优 SDK 能力 + 风险兜底机制”,帮助开发者在教学场景中稳定复用。学习 SDK 不应停留在接口调用层面,而要围绕具体场景痛点转化为实用功能,从而更快落地高质量的 AR 教学协同应用。
- 点赞
- 收藏
- 关注作者
评论(0)