跳到主要内容
AR 健身教练“形随心动”:基于 Rokid CXR-M SDK 的实战开发 | 极客日志
Kotlin AI 大前端 java 算法
AR 健身教练“形随心动”:基于 Rokid CXR-M SDK 的实战开发 居家健身缺乏专业指导及低头看屏体验差是常见痛点,基于 Rokid CXR-M SDK 构建 AR 健身应用“形随心动”可解决这一问题。项目通过手机端与眼镜协同,利用 AI 场景实现动作识别,自定义页面渲染 AR 教练界面,结合提词器进行实时纠正。内容涵盖 SDK 配置、权限管理、架构设计、核心功能代码实现及设备连接、动作捕捉、数据可视化等关键步骤,并分享性能优化方案,为开发者提供 AR 交互落地的参考案例。
狂少 发布于 2026/3/24 更新于 2026/5/19 11 浏览项目背景与创意起源
在快节奏的都市生活中,健身成了保持健康的重要方式。但居家健身有个老毛病:缺乏专业指导,动作不规范容易受伤,而且低头看手机或平板也破坏了沉浸感。
根据《2024 年中国健身行业白皮书》,超过 65% 的居家用户因为'缺乏专业指导'放弃健身。Rokid Glasses 这种轻量级 AR 眼镜,'抬头即见'的交互正好能解决这个问题。
'形随心动'这个想法很简单:把专业教练'投射'到视野里,实时指导动作,还能给数据反馈。利用 Rokid CXR-M SDK 的 AI 场景、自定义页面和提词器功能,这事儿就能落地。
Rokid CXR-M SDK 相关
1. SDK 介绍
CXR-M SDK 是面向移动端的开发工具包,主要用来构建手机端与 Rokid Glasses 的控制和协同应用。通过它,开发者能和眼镜建立稳定连接,实现数据通信、实时音视频获取以及场景自定义。目前只支持 Android 版本。
2. SDK 开发导入配置
配置 Maven 仓库
CXR-M SDK 采用 Maven 在线管理。找到 settings.gradle.kts,在 dependencyResolutionManagement 节点的 repositories 中添加仓库地址:
https: //maven.rokid.com/repository /maven-public/
依赖导入
在 build.gradle.kts 的 dependencies 节点添加依赖包:
implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2" )
注意:SDK 要求 minSdk >= 28。如果项目中已有版本冲突,优先选用 SDK 对应的版本。
3. 权限申请
声明权限
CXR-M SDK 需要网络、Wi-Fi、蓝牙等权限(蓝牙权限需同步申请 FINE_LOCATION)。在 AndroidManifest.xml 中声明最小权限集:
<?xml version="1.0" encoding="utf-8" ?>
<manifest xmlns:android ="http://schemas.android.com/apk/res/android"
xmlns:tools ="http://schemas.android.com/tools" >
<uses-permission android:name ="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name ="android.permission.ACCESS_FINE_LOCATION" />
<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_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" />
<uses-permission android:name ="android.permission.INTERNET" />
</manifest >
使用前必须先进行必要权限的动态申请,否则 SDK 不可用。
class MainActivity : AppCompatActivity () {
companion object {
const val TAG = "MainActivity"
const val REQUEST_CODE_PERMISSIONS = 100
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()
}
private val permissionGrantedResult = MutableLiveData<Boolean ?>()
override fun onCreate (savedInstanceState: Bundle ?) {
super .onCreate(savedInstanceState)
requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
permissionGrantedResult.observe(this ) { granted ->
if (granted == true ) {
} else {
}
}
}
override fun onRequestPermissionsResult (
requestCode: Int ,
permissions: Array <out String >,
grantResults: IntArray
) {
super .onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS.hashCode()) {
val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
permissionGrantedResult.postValue(allGranted)
}
}
}
技术架构设计
1. 整体架构 应用层(Android App) → CXR-M SDK → Rokid Glasses(YodaOS) → 硬件传感器
应用层 :提供 UI、健身计划管理、社交功能。
CXR-M SDK :负责与眼镜通信、AI 场景处理、AR 界面渲染。
Rokid Glasses :执行 AR 渲染、动作捕捉、数据反馈。
硬件传感器 :提供姿态、心率等原始数据。
应用层 :包含业务逻辑。
CXR-M SDK :核心通信与功能调用。
Rokid Glasses :硬件平台。
AR 界面 :自定义页面实现的可视化展示。
AI 场景 :利用 SDK 能力实现动作识别。
社交互动模块 :好友挑战、进度分享。
2. 核心组件设计 组件 功能 SDK 实现方式 实时动作识别模块 摄像头捕捉动作,分析质量 AI 场景、自定义页面 个性化计划模块 定制健身计划 自定义页面、数据同步 AR 教练界面 3D 显示虚拟教练,实时指导 自定义页面、提词器 数据可视化模块 实时显示心率、卡路里 自定义页面、媒体操作 社交互动模块 好友挑战、进度分享 数据通信、自定义页面
3. 架构设计优势
分层解耦 :业务逻辑与硬件交互分离,方便扩展维护。
硬件能力充分利用 :摄像头做动作捕捉,IMU 辅助分析,AR 提供沉浸体验。
SDK 深度整合 :AI 场景做识别,自定义页面做界面,提词器做文字指导。
可扩展性 :新功能只需新增模块,不影响底层。
组件 关键技术点 SDK 实现方式 难度 设备连接 蓝牙稳定连接 CxrApi.getInstance().initBluetooth()★★☆ AI 场景 动作识别与分析 setAiEventListener() + controlScene()★★★★ AR 界面 3D 指导界面渲染 openCustomView() + JSON 布局★★★ 数据同步 实时数据传输 sendStream() + setMediaFilesUpdateListener()★★ 社交功能 好友挑战实现 controlScene() + 数据通信★★★
关键功能实现详解
1. 设备连接与初始化 搞定设备连接是第一步,毕竟没有它后续都免谈。这里要注意权限检查,确保蓝牙扫描正常。
class FitnessApp : AppCompatActivity () {
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()
override fun onCreate (savedInstanceState: Bundle ?) {
super .onCreate(savedInstanceState)
if (!hasPermissions(REQUIRED_PERMISSIONS)) {
requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
} else {
initializeDeviceConnection()
}
}
private fun hasPermissions (permissions: Array <String >) : Boolean {
return permissions.all { ContextCompat.checkSelfPermission(this , it) == PackageManager.PERMISSION_GRANTED }
}
private fun initializeDeviceConnection () {
val scanner = BluetoothLeScannerCompat.getScanner()
scanner.startScan(
listOf(ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("00009100-0000-1000-8000-00805f9b34fb" ))
.build()),
ScanSettings.Builder().build(),
scanCallback
)
}
private val scanCallback = object : ScanCallback() {
override fun onScanResult (callbackType: Int , result: ScanResult ) {
super .onScanResult(callbackType, result)
val device = result.device
CxrApi.getInstance().initBluetooth(this @FitnessApp , device, object : BluetoothStatusCallback {
override fun onConnectionInfo (socketUuid: String ?, macAddress: String ?, rokidAccount: String ?, glassesType: Int ) {
Log.d("FitnessApp" , "Connection info: $socketUuid , $macAddress , $rokidAccount , $glassesType " )
CxrApi.getInstance().connectBluetooth(this @FitnessApp , socketUuid, macAddress, this @FitnessApp )
}
override fun onConnected () {
Log.d("FitnessApp" , "Bluetooth connected successfully" )
initializeAiScene()
}
override fun onDisconnected () {
Log.e("FitnessApp" , "Bluetooth disconnected" )
}
override fun onFailed (errorCode: ValueUtil .CxrBluetoothErrorCode ?) {
Log.e("FitnessApp" , "Bluetooth connection failed: $errorCode " )
}
})
}
}
private fun initializeAiScene () {
CxrApi.getInstance().setAiEventListener(object : AiEventListener {
override fun onAiEvent (eventType: ValueUtil .CxrAiEventType ?, data : String ?) {
when (eventType) {
ValueUtil.CxrAiEventType.ACTION_RECOGNITION -> {
handleActionRecognition(data )
}
}
}
})
CxrApi.getInstance().controlScene(ValueUtil.CxrSceneType.AI_SCENE, true , null )
}
}
2. 实时动作捕捉与纠正 这是核心功能,利用眼镜的摄像头和 AI 能力实时分析。这里要注意阈值设置,太高了不灵敏,太低了误报多。
private fun startExercise (exerciseType: String ) {
val actionParams = HashMap<String, String>()
actionParams["exerciseType" ] = exerciseType
actionParams["threshold" ] = "0.85"
CxrApi.getInstance().controlScene(
ValueUtil.CxrSceneType.AI_SCENE, true , actionParams
)
CxrApi.getInstance().setAiEventListener(object : AiEventListener {
override fun onAiEvent (eventType: ValueUtil .CxrAiEventType ?, data : String ?) {
when (eventType) {
ValueUtil.CxrAiEventType.ACTION_RECOGNITION -> {
val recognitionResult = Gson().fromJson(data , ActionRecognitionResult::class .java)
if (recognitionResult.accuracy > 0.8 ) {
showPositiveFeedback(recognitionResult.exerciseName, "动作标准!" )
} else {
showCorrectionFeedback(recognitionResult.exerciseName, recognitionResult.correctionMessage)
}
}
}
}
})
}
private fun showCorrectionFeedback (exerciseName: String , message: String ) {
CxrApi.getInstance().controlScene(
ValueUtil.CxrSceneType.WORD_TIPS, true , null
)
val text = "动作纠正:$message "
CxrApi.getInstance().sendStream(
ValueUtil.CxrStreamType.WORD_TIPS, text.toByteArray(), "correction_text" ,
object : SendStatusCallback {
override fun onSendSucceed () {
Log.d("FitnessApp" , "Correction message sent successfully" )
}
override fun onSendFailed (errorCode: ValueUtil .CxrSendErrorCode ?) {
Log.e("FitnessApp" , "Failed to send correction message: $errorCode " )
}
}
)
}
private fun setupARCoachInterface () {
val arCoachJson = """
{
"type": "LinearLayout",
"props": {
"layout_width": "match_parent",
"layout_height": "match_parent",
"orientation": "vertical",
"gravity": "center_horizontal",
"paddingTop": "140dp",
"paddingBottom": "100dp",
"backgroundColor": "#00000000"
},
"children": [
{
"type": "TextView",
"props": {
"id": "tv_instruction",
"layout_width": "wrap_content",
"layout_height": "wrap_content",
"text": "请保持背部挺直",
"textSize": "18sp",
"textColor": "#FF00FF00",
"textStyle": "bold",
"marginBottom": "20dp"
}
},
{
"type": "TextView",
"props": {
"id": "tv_count",
"layout_width": "wrap_content",
"layout_height": "wrap_content",
"text": "0/10",
"textSize": "24sp",
"textColor": "#FFFFFFFF",
"textStyle": "bold",
"marginBottom": "30dp"
}
}
]
}
""" .trimIndent()
CxrApi.getInstance().openCustomView(arCoachJson)
CxrApi.getInstance().setCustomViewListener(object : CustomViewListener {
override fun onCustomViewOpened () {
Log.d("FitnessApp" , "Custom view opened successfully" )
}
override fun onCustomViewClosed () {
Log.d("FitnessApp" , "Custom view closed" )
}
})
}
3. 个性化健身计划 根据用户身体数据和目标定制计划。这部分逻辑可以放在后端,也可以本地生成,示例中为了简化直接返回预设计划。
class PersonalizedPlanActivity : AppCompatActivity () {
override fun onCreate (savedInstanceState: Bundle ?) {
super .onCreate(savedInstanceState)
setContentView(R.layout.activity_personalized_plan)
val age = findViewById<EditText>(R.id.et_age).text.toString().toInt()
val weight = findViewById<EditText>(R.id.et_weight).text.toString().toFloat()
val height = findViewById<EditText>(R.id.et_height).text.toString().toFloat()
val goal = findViewById<Spinner>(R.id.spinner_goal).selectedItem.toString()
val plan = generatePersonalizedPlan(age, weight, height, goal)
displayPlan(plan)
savePlanToGlasses(plan)
}
private fun generatePersonalizedPlan (age: Int , weight: Float , height: Float , goal: String ) : Plan {
return Plan(
"30 天减脂计划" ,
listOf(
Exercise("深蹲" , "3 组×15 次" , "保持膝盖不超过脚尖" , "squats" ),
Exercise("平板支撑" , "3 组×30 秒" , "保持身体成直线" , "plank" ),
Exercise("跳绳" , "3 组×2 分钟" , "保持手腕放松" , "jump_rope" )
)
)
}
private fun displayPlan (plan: Plan ) {
val planAdapter = PlanAdapter(plan.exercises)
findViewById<RecyclerView>(R.id.rv_plan).adapter = planAdapter
}
private fun savePlanToGlasses (plan: Plan ) {
val planJson = Gson().toJson(plan)
CxrApi.getInstance().sendStream(
ValueUtil.CxrStreamType.PERSONALIZED_PLAN, planJson.toByteArray(), "fitness_plan" ,
object : SendStatusCallback {
override fun onSendSucceed () {
Log.d("FitnessApp" , "Plan sent to glasses successfully" )
}
override fun onSendFailed (errorCode: ValueUtil .CxrSendErrorCode ?) {
Log.e("FitnessApp" , "Failed to send plan: $errorCode " )
}
}
)
}
}
4. 数据可视化与实时反馈 让数据浮现在眼前,不用低头看手表。这里要注意主线程更新 UI 的问题,避免卡顿。
class DataVisualizationActivity : AppCompatActivity () {
private var heartRate = 0
private var caloriesBurned = 0f
private var exerciseTime = 0
override fun onCreate (savedInstanceState: Bundle ?) {
super .onCreate(savedInstanceState)
setContentView(R.layout.activity_data_visualization)
setupARDataView()
startExerciseTimer()
startDataUpdates()
}
private fun setupARDataView () {
val dataViewJson = """
{
"type": "LinearLayout",
"props": {
"layout_width": "match_parent",
"layout_height": "match_parent",
"orientation": "vertical",
"gravity": "center",
"backgroundColor": "#00000000"
},
"children": [
{
"type": "TextView",
"props": {
"id": "tv_heart_rate",
"layout_width": "wrap_content",
"layout_height": "wrap_content",
"text": "心率:72",
"textSize": "20sp",
"textColor": "#FF00FF00",
"textStyle": "bold"
}
},
{
"type": "TextView",
"props": {
"id": "tv_calories",
"layout_width": "wrap_content",
"layout_height": "wrap_content",
"text": "卡路里:0",
"textSize": "20sp",
"textColor": "#FF00FF00",
"textStyle": "bold",
"marginTop": "20dp"
}
},
{
"type": "TextView",
"props": {
"id": "tv_time",
"layout_width": "wrap_content",
"layout_height": "wrap_content",
"text": "时间:00:00",
"textSize": "20sp",
"textColor": "#FF00FF00",
"textStyle": "bold",
"marginTop": "20dp"
}
}
]
}
""" .trimIndent()
CxrApi.getInstance().openCustomView(dataViewJson)
}
private fun updateDataView () {
val updateData = listOf(
UpdateAction("update" , "tv_heart_rate" , "text" , "心率:$heartRate " ),
UpdateAction("update" , "tv_calories" , "text" , "卡路里:$caloriesBurned " ),
UpdateAction("update" , "tv_time" , "text" , "时间:${formatTime(exerciseTime)} " )
)
CxrApi.getInstance().updateCustomView(Gson().toJson(updateData))
}
private fun formatTime (seconds: Int ) : String {
val minutes = seconds / 60
val remainingSeconds = seconds % 60
return String.format("%02d:%02d" , minutes, remainingSeconds)
}
private fun startDataUpdates () {
val handler = Handler(Looper.getMainLooper())
handler.postDelayed(object : Runnable {
override fun run () {
heartRate = (heartRate + 2 ).coerceAtMost(160 )
caloriesBurned += 0.5f
exerciseTime += 2
updateDataView()
handler.postDelayed(this , 2000 )
}
}, 2000 )
}
private fun startExerciseTimer () {
val startTime = System.currentTimeMillis()
val timerHandler = Handler(Looper.getMainLooper())
timerHandler.postDelayed(object : Runnable {
override fun run () {
exerciseTime = ((System.currentTimeMillis() - startTime) / 1000 ).toInt()
updateDataView()
timerHandler.postDelayed(this , 1000 )
}
}, 1000 )
}
}
5. 社交健身挑战 和朋友一起练更有动力。这里只是模拟发送挑战,实际项目中需要对接社交 API。
class ChallengeActivity : AppCompatActivity () {
private val challenges = mutableListOf<Challenge>(
Challenge("7 天深蹲挑战" , "完成 7 天深蹲训练,每天 100 次" , 7 , 100 ),
Challenge("30 天心率挑战" , "保持心率在 120-150 之间" , 30 , 0 )
)
override fun onCreate (savedInstanceState: Bundle ?) {
super .onCreate(savedInstanceState)
setContentView(R.layout.activity_challenge)
setupChallengeList()
findViewById<Button>(R.id.btn_add_friend_challenge).setOnClickListener {
showAddFriendChallengeDialog()
}
}
private fun setupChallengeList () {
val adapter = ChallengeAdapter(challenges)
findViewById<RecyclerView>(R.id.rv_challenges).adapter = adapter
}
private fun showAddFriendChallengeDialog () {
val builder = AlertDialog.Builder(this )
builder.setTitle("添加好友挑战" )
val input = EditText(this )
input.hint = "输入好友 ID"
builder.setView(input)
builder.setPositiveButton("确定" ) { _, _ ->
val friendId = input.text.toString()
createFriendChallenge(friendId)
}
builder.setNegativeButton("取消" , null )
builder.show()
}
private fun createFriendChallenge (friendId: String ) {
val challenge = Challenge(
"与$friendId 的挑战" , "一起完成 30 天健身挑战" , 30 , 0
)
saveChallengeToGlasses(challenge)
sendChallengeToFriend(friendId, challenge)
}
private fun saveChallengeToGlasses (challenge: Challenge ) {
val challengeJson = Gson().toJson(challenge)
CxrApi.getInstance().sendStream(
ValueUtil.CxrStreamType.CHALLENGE, challengeJson.toByteArray(), "challenge_${challenge.id} " ,
object : SendStatusCallback {
override fun onSendSucceed () {
Log.d("FitnessApp" , "Challenge saved to glasses" )
}
override fun onSendFailed (errorCode: ValueUtil .CxrSendErrorCode ?) {
Log.e("FitnessApp" , "Failed to save challenge: $errorCode " )
}
}
)
}
private fun sendChallengeToFriend (friendId: String , challenge: Challenge ) {
Log.d("FitnessApp" , "Challenge sent to friend $friendId " )
Toast.makeText(this , "挑战已发送给好友 $friendId " , Toast.LENGTH_SHORT).show()
}
}
挑战与解决方案
1. 动作识别精度问题
多模态数据融合:结合摄像头和传感器数据。
动态阈值调整:根据用户特征自动调整。
用户反馈机制:允许确认或纠正结果。
private fun adjustRecognitionThreshold (userProfile: UserProfile ) : Float {
return when {
userProfile.age < 25 -> 0.85f
userProfile.age < 40 -> 0.80f
else -> 0.75f
}
}
2. AR 界面流畅度优化
优化 JSON 布局,减少元素。
使用 setCustomViewListener 优化渲染。
异步数据更新,避免阻塞主线程。
CxrApi.getInstance().setCustomViewListener(object : CustomViewListener {
override fun onCustomViewOpened () {
CxrApi.getInstance().setCustomViewRenderMode(ValueUtil.CxrRenderMode.FAST)
}
override fun onCustomViewClosed () {
CxrApi.getInstance().setCustomViewRenderMode(ValueUtil.CxrRenderMode.DEFAULT)
}
})
3. 电池消耗问题
智能功耗管理:动态调整。
自动休眠:停止运动后低功耗模式。
优化传输:减少不必要同步。
CxrApi.getInstance().setPowerOffTimeout(30 )
CxrApi.getInstance().setScreenOffTimeout(60 )
val exerciseTimer = object : CountDownTimer(300000 , 1000 ) {
override fun onTick (millisUntilFinished: Long ) {}
override fun onFinish () {
CxrApi.getInstance().setGlassBrightness(3 )
CxrApi.getInstance().setGlassVolume(2 )
Toast.makeText(this @FitnessApp , "进入低功耗模式" , Toast.LENGTH_SHORT).show()
}
}.start()
未来展望 '形随心动'验证了 Rokid CXR-M SDK 在 AR 健身场景的能力。接下来我们会继续拓展:
AI 能力增强 :集成更先进的模型,提升精度。
内容生态扩展 :合作专业教练,提供更多课程。
硬件联动 :对接智能设备,获取更全面数据。
跨平台支持 :拓展更多硬件设备。
结语 '形随心动'不仅是一款应用,更是 Rokid CXR-M SDK 能力的展示。通过深度整合 AI 场景、自定义页面和提词器,我们实现了'抬头即见指导'的体验,解决了居家健身的核心痛点。
这展示了 Rokid 生态的应用前景,也为开发者提供了参考案例。随着 SDK 完善和生态扩展,AR 技术将在更多生活场景中发挥作用。未来将继续深耕 AR 健身领域,为用户提供更专业的健身体验。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online