【征文计划】Rokid CXR-M SDK全解析:从设备连接到语音交互的AR协同开发指南
引言
在智能穿戴设备日益普及的今天,AR眼镜作为新一代人机交互设备,正逐步改变着我们的工作与生活方式。Rokid CXR-M SDK作为专为Rokid Glasses设计的开发工具包,为开发者提供了丰富而强大的功能,使得手机与AR眼镜之间的协同交互变得前所未有的便捷与高效。本文通过详细解析Rokid CXR-M SDK的核心功能、技术原理及应用实践,帮助开发者快速上手并构建出优秀的AR应用。
一、Rokid CXR-M SDK介绍
Rokid CXR-M SDK是面向移动端(目前仅支持Android)的开发工具包,它使得开发者能够轻松构建与Rokid Glasses进行控制和协同的应用。通过SDK,开发者可以实现手机与AR眼镜之间的稳定连接、数据通信、实时音视频传输以及复杂的场景自定义等功能。无论是进行界面交互、远程控制还是与眼镜端配合完成特定任务,Rokid CXR-M SDK都能提供全面的支持。
主要功能特性
-
设备连接与管理:支持通过蓝牙和Wi-Fi与Rokid Glasses建立稳定连接,并获取设备状态信息。
-
自定义场景交互:快速接入YodaOS-Sprite操作系统定义的场景交互流程,实现自定义功能开发。
-
AI助手服务:高效利用Rokid Assist Service中的服务,包括文件互传、录音、拍照等功能。
-
实时音视频传输:支持眼镜端音视频数据的实时采集与传输至手机端进行处理。
-
数据同步与媒体管理:支持眼镜端与手机端之间的媒体文件同步与管理。
二、核心技术原理
1. 设备连接机制
以蓝牙为基础控制通道、Wi-Fi P2P 为高带宽补充,蓝牙按 “扫描→初始化→连接” 流程,用 UUID 过滤设备,Wi-Fi 需蓝牙先连,双通道均有专属回调监听状态。
核心代码示例:
import com.rokid.cxr.api.CxrApi
import com.rokid.cxr.callback.{BluetoothStatusCallback, WifiP2PStatusCallback}
import com.rokid.cxr.util.ValueUtil
// 蓝牙连接核心
fun connectBluetooth(device: BluetoothDevice) {
CxrApi.getInstance().initBluetooth(appContext, device, object : BluetoothStatusCallback {
override fun onConnectionInfo(uuid: String?, mac: String?, _, _) {
if (uuid != null && mac != null) {
CxrApi.getInstance().connectBluetooth(appContext, uuid, mac, this)
}
}
override fun onConnected() = Unit
override fun onFailed(_) = Unit
override fun onDisconnected() = Unit
})
}
// Wi-Fi P2P初始化(依赖蓝牙)
fun initWifi() {
if (CxrApi.getInstance().isBluetoothConnected) {
CxrApi.getInstance().initWifiP2P(object : WifiP2PStatusCallback {
override fun onConnected() = Unit
override fun onFailed(_) = Unit
override fun onDisconnected() = Unit
})
}
}
2. 实时音视频传输
音频支持 PCM/Opus 编码,蓝牙传输;视频固定 30fps,先存后传。按带宽分层传输,用回调控延迟,适配不同场景需求。
核心代码示例:
import com.rokid.cxr.api.CxrApi
import com.rokid.cxr.callback.AudioStreamListener
import com.rokid.cxr.util.ValueUtil
// 音频采集(PCM编码)
fun startAudioCollect() {
if (CxrApi.getInstance().isBluetoothConnected) {
CxrApi.getInstance().setAudioStreamListener(object : AudioStreamListener {
override fun onAudioStream(data: ByteArray?, o: Int, l: Int) {
data?.copyOfRange(o, o + l)?.let { /* 传ASR */ }
}
override fun onStartAudioStream(_, _) = Unit
})
CxrApi.getInstance().openAudioRecord(1, "AI_assistant")
}
}
// 视频参数配置
fun setVideoParams() {
CxrApi.getInstance().setVideoParams(5, 30, 1920, 1080, 1)
CxrApi.getInstance().controlScene(ValueUtil.CxrSceneType.VIDEO_RECORD, true, null)
}
3. 数据同步与场景管理
蓝牙查未同步媒体数,Wi-Fi 传文件;场景分四类,用controlScene()启停,专属接口配参,回调监状态。
核心代码示例:
import com.rokid.cxr.api.CxrApi
import com.rokid.cxr.callback.{SyncStatusCallback, SendStatusCallback}
import com.rokid.cxr.util.ValueUtil
// 媒体同步
fun syncMedia(savePath: String) {
if (CxrApi.getInstance().isWifiP2PConnected) {
CxrApi.getInstance().startSync(savePath,
arrayOf(ValueUtil.CxrMediaType.ALL), object : SyncStatusCallback {
override fun onSyncStart() = Unit
override fun onSingleFileSynced(_) = Unit
override fun onSyncFailed() = Unit
override fun onSyncFinished() = Unit
})
}
}
// 提词器场景控制
fun controlWordTips(open: Boolean, content: String) {
CxrApi.getInstance().controlScene(ValueUtil.CxrSceneType.WORD_TIPS, open, null)
if (open) CxrApi.getInstance().sendStream(ValueUtil.CxrStreamType.WORD_TIPS,
content.toByteArray(), "tips.txt", object : SendStatusCallback {
override fun onSendSucceed() = Unit
override fun onSendFailed(_) = Unit
})
}
三、应用实践:基于 Rokid CXR-M SDK 实现手机与眼镜的语音协同交互解析
在工业巡检、远程协助等 AR 场景中,工程师佩戴 Rokid Glasses 时,“双手解放 + 无屏幕交互” 的需求对语音控制提出强依赖 —— 需通过自然语音完成 “唤醒响应、指令下发、结果反馈”。传统方案或旧 SDK 存在 “本地唤醒抗噪弱、语音传输延迟高” 等问题,而 Rokid CXR-M SDK(仅 Android 端)通过 “眼镜语音采集→手机端处理→指令 / 反馈回传” 的协同架构,完美解决该痛点。
1、场景定义与 SDK 语音能力适配
1.1 核心场景
针对 Rokid Glasses 的 “解放双手” 交互需求,设计以下语音协同场景:
-
场景 1:眼镜端拾音→手机端识别:用户通过自然语音发起操作需求,眼镜内置麦克风激活拾音,CXR-M SDK 将音频流实时传输至手机端,支持边采集边传输,断连后可从眼镜端拉取完整音频文件
-
场景 2:语音指令控制:手机解析 ASR 结果后,生成控制指令,通过 CXR-M SDK 下发至眼镜执行;
-
场景 3:语音反馈播放:眼镜执行指令后,手机端生成语音合成(TTS)数据,下发至眼镜端播放,形成交互闭环。
1.2 CXR-M SDK 语音相关能力
根据官方文档,CXR-M SDK 支持以下语音核心能力,为场景落地提供基础:
-
眼镜端实时音频采集(支持 16kHz/16bit 单声道 PCM 格式);
-
双向音频数据传输(低延迟,满足语音实时交互需求,延迟≤100ms);
-
眼镜端音频播放(支持 PCM 音频流解码,用于播放手机下发的 TTS 反馈)。
2.1 环境与依赖
2.1.1 基础环境要求
-
操作系统:Android 9.0(API 28)及以上
-
开发工具:Android Studio 4.0+
-
硬件:Rokid Glasses、Android 手机(支持蓝牙 5.0+、Wi-Fi Direct)
2.1.2 SDK 导入
CXR-M SDK 采用 Maven 在线管理,需按官方文档配置仓库与依赖:
// 1. 项目级settings.gradle.kts(配置官方Maven仓库)
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\.android.*")
includeGroupByRegex("com\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
// 官方CXR-M SDK Maven仓库(必须配置)
maven { url = uri("https://maven.rokid.com/repository/maven-public/") }
google()
mavenCentral()
}
}
// 2. 应用级build.gradle.kts(导入依赖,版本与官方一致)
android {
defaultConfig {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a" // 官方支持的架构
}
minSdk = 28 // 严格遵循官方最低版本要求
}
}
dependencies {
// 核心:CXR-M SDK官方依赖(版本号与官方文档一致)
implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-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("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.12.0")
}
2.2 权限配置
2.2.1 声明权限(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">
<!-- 1. 蓝牙相关权限(文档“权限申请”章节核心,语音传输主通道) -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 2. 定位权限(文档强制要求:蓝牙扫描需同步申请,用于设备发现) -->
<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_SCAN" android:usesPermissionFlags="neverForLocation" tools:targetApi="s" />
<!-- 3. 网络/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" />
<!-- 4. 语音采集权限(文档“录音功能”隐含要求,用于眼镜端语音转发至手机) -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.VoiceCooperate">
<!-- 声明主Activity(需替换为实际Activity路径) -->
<activity
android:name=".VoiceCooperateActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
2.2.2 动态申请权限
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
import com.rokid.cxr.client.CxrApi
class VoiceCooperateActivity : AppCompatActivity() {
companion object {
const val REQUEST_CODE_PERMISSIONS = 100
/**
* 动态申请权限列表(严格按文档“必需权限”+ Android版本拆分)
* - 无需动态申请BLUETOOTH/BLUETOOTH_ADMIN,仅需SCAN/CONNECT
* - 需动态申请BLUETOOTH/BLUETOOTH_ADMIN + 定位 + 录音
*/
private val REQUIRED_PERMISSIONS: Array<String>
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
} else {
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN
)
}
}
// 权限申请结果监听(用于触发SDK初始化)
private val permissionGranted = MutableLiveData<Boolean?>(null)
// CXR-M SDK核心实例(文档推荐单例模式)
private val cxrApi by lazy { CxrApi.getInstance() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_voice_cooperate)
// 步骤1:先检查是否已授予所有必需权限(避免重复申请,提升用户体验)
if (checkAllPermissionsGranted()) {
permissionGranted.postValue(true)
} else {
// 步骤2:未授予则发起动态申请(文档推荐在onCreate中执行)
requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
// 步骤3:监听权限结果,通过后初始化CXR-M SDK(文档要求:权限不足时SDK不可用)
permissionGranted.observe(this) { isGranted ->
when (isGranted) {
true -> initCXRMSDK() // 权限通过,初始化SDK
false -> {
// 明确提示缺失权限,而非笼统提示(文档未明说,但符合场景需求)
Toast.makeText(
this,
"必需权限未授予(定位/蓝牙/录音),无法使用语音协同功能",
Toast.LENGTH_LONG
).show()
finish() // 权限不足,退出页面(文档隐含:权限不足SDK不可用)
}
null -> {} // 初始状态,无操作
}
}
}
/**
* 检查是否已授予所有必需权限(文档未明说,但实际开发必需)
*/
private fun checkAllPermissionsGranted(): Boolean {
return REQUIRED_PERMISSIONS.all {
checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
}
}
/**
* 权限申请结果回调
*/
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 }
permissionGranted.postValue(allGranted)
// 提示缺失的具体权限
if (!allGranted) {
val deniedPermissions = permissions.filterIndexed { index, _ ->
grantResults[index] != PackageManager.PERMISSION_GRANTED
}.joinToString("、")
Toast.makeText(
this,
"以下权限被拒绝:$deniedPermissions,请在设置中手动授予",
Toast.LENGTH_LONG
).show()
}
}
}
/**
* 初始化CXR-M SDK
*/
private fun initCXRMSDK() {
// 此处可执行SDK初始化逻辑(如蓝牙扫描、录音初始化等,参考文档“设备连接”章节)
Toast.makeText(this, "权限通过,CXR-M SDK初始化中...", Toast.LENGTH_SHORT).show()
// 示例:初始化蓝牙辅助类(后续步骤,参考文档“Bluetooth连接”章节)
// initBluetoothHelper()
}
override fun onDestroy() {
super.onDestroy()
// 销毁时反初始化SDK资源(如蓝牙、录音)
cxrApi.deinitBluetooth()
}
}
2.3核心语音协同功能实现
2.3.1 步骤 1:初始化 CXR-M SDK 与蓝牙连接
语音功能完全依赖蓝牙连接,需按 “权限检查→蓝牙扫描(官方 UUID 过滤)→初始化蓝牙→建立连接” 的官方流程执行,适配工业场景 “一次配对、多次复用” 需求(减少现场重复操作)。
import android.Manifest
import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import com.rokid.cxr.api.CxrApi
import com.rokid.cxr.callback.BluetoothStatusCallback
import com.rokid.cxr.util.ValueUtil
class RokidBluetoothInitializer(
private val activity: Activity,
private val onInitSuccess: (() -> Unit)? = null,
private val onInitFailed: ((errorMsg: String) -> Unit)? = null
) {
private val TAG = "RokidBtInit"
// 蓝牙请求码
private val REQUEST_ENABLE_BT = 1001
// 权限请求码
private val REQUEST_PERMISSIONS = 1002
// Rokid眼镜蓝牙服务UUID(用于过滤设备)
private val ROKID_GLASSES_UUID = "00009100-0000-1000-8000-00805f9b34fb"
// 已发现的Rokid眼镜设备
private var targetGlassesDevice: BluetoothDevice? = null
/**
* 启动初始化流程:权限检查 → 蓝牙开启 → 设备扫描 → SDK蓝牙初始化
*/
fun startInit() {
if (checkPermissions()) {
checkBluetoothEnable()
} else {
requestPermissions()
}
}
/**
* 检查必要权限(蓝牙+定位)
*/
private fun checkPermissions(): Boolean {
val requiredPermissions = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN
).apply {
// Android 12及以上需额外申请蓝牙扫描/连接权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
add(Manifest.permission.BLUETOOTH_SCAN)
add(Manifest.permission.BLUETOOTH_CONNECT)
}
}
return requiredPermissions.all {
activity.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
}
}
/**
* 请求必要权限
*/
private fun requestPermissions() {
val requiredPermissions = 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()
activity.requestPermissions(requiredPermissions, REQUEST_PERMISSIONS)
}
/**
* 检查蓝牙是否开启,未开启则请求开启
*/
private fun checkBluetoothEnable() {
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
if (bluetoothAdapter == null) {
onInitFailed?.invoke("设备不支持蓝牙")
return
}
if (!bluetoothAdapter.isEnabled) {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
activity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
} else {
scanRokidGlasses(bluetoothAdapter)
}
}
/**
* 扫描Rokid眼镜设备(通过UUID过滤)
*/
private fun scanRokidGlasses(bluetoothAdapter: BluetoothAdapter) {
Log.d(TAG, "开始扫描Rokid眼镜...")
// 先检查已配对设备(避免重复扫描)
val bondedDevices = bluetoothAdapter.bondedDevices
for (device in bondedDevices) {
if (device.name?.contains("Glasses") == true) {
targetGlassesDevice = device
initSdkBluetooth(device)
return
}
}
// 未找到已配对设备,启动蓝牙扫描(需结合官方BluetoothHelper,此处简化)
val bluetoothHelper = BluetoothHelper(
context = activity,
initStatus = {},
deviceFound = {
// 从扫描结果中获取第一个Rokid眼镜设备
val device = bluetoothHelper.scanResultMap.values.firstOrNull {
it.name?.contains("Glasses") == true
}
if (device != null) {
targetGlassesDevice = device
bluetoothHelper.stopScan() // 找到设备后停止扫描
initSdkBluetooth(device)
}
}
)
bluetoothHelper.checkPermissions()
bluetoothHelper.startScan()
}
/**
* 初始化SDK蓝牙连接(核心步骤)
*/
private fun initSdkBluetooth(device: BluetoothDevice) {
Log.d(TAG, "初始化SDK蓝牙连接,设备名:${device.name},MAC:${device.address}")
CxrApi.getInstance().initBluetooth(
context = activity.applicationContext,
device = device,
callback = object : BluetoothStatusCallback {
// 连接信息回调(获取UUID和MAC,用于后续连接)
override fun onConnectionInfo(
socketUuid: String?,
macAddress: String?,
rokidAccount: String?,
glassesType: Int
) {
Log.d(TAG, "获取连接信息:UUID=$socketUuid,MAC=$macAddress,眼镜类型=$glassesType")
if (socketUuid.isNullOrEmpty() || macAddress.isNullOrEmpty()) {
onInitFailed?.invoke("获取连接信息失败,UUID或MAC为空")
return
}
// 调用SDK连接接口建立蓝牙连接
connectToGlasses(socketUuid, macAddress)
}
// 蓝牙连接成功回调
override fun onConnected() {
Log.d(TAG, "蓝牙连接成功")
onInitSuccess?.invoke()
}
// 蓝牙断开回调
override fun onDisconnected() {
Log.w(TAG, "蓝牙连接断开")
onInitFailed?.invoke("蓝牙连接已断开")
}
// 连接失败回调
override fun onFailed(errorCode: ValueUtil.CxrBluetoothErrorCode?) {
val errorMsg = when (errorCode) {
ValueUtil.CxrBluetoothErrorCode.PARAM_INVALID -> "参数无效"
ValueUtil.CxrBluetoothErrorCode.BLE_CONNECT_FAILED -> "BLE连接失败"
ValueUtil.CxrBluetoothErrorCode.SOCKET_CONNECT_FAILED -> "Socket连接失败"
else -> "未知错误(错误码:$errorCode)"
}
Log.e(TAG, "蓝牙初始化失败:$errorMsg")
onInitFailed?.invoke(errorMsg)
}
}
)
}
/**
* 建立SDK蓝牙连接
*/
private fun connectToGlasses(socketUuid: String, macAddress: String) {
CxrApi.getInstance().connectBluetooth(
context = activity.applicationContext,
socketUuid = socketUuid,
macAddress = macAddress,
callback = object : BluetoothStatusCallback {
override fun onConnected() {
Log.d(TAG, "SDK蓝牙连接确认成功")
}
override fun onDisconnected() {
Log.w(TAG, "SDK蓝牙连接断开")
}
override fun onFailed(errorCode: ValueUtil.CxrBluetoothErrorCode?) {
val errorMsg = when (errorCode) {
ValueUtil.CxrBluetoothErrorCode.PARAM_INVALID -> "连接参数无效"
ValueUtil.CxrBluetoothErrorCode.BLE_CONNECT_FAILED -> "BLE连接失败"
ValueUtil.CxrBluetoothErrorCode.SOCKET_CONNECT_FAILED -> "Socket连接失败"
else -> "连接失败(错误码:$errorCode)"
}
onInitFailed?.invoke(errorMsg)
}
override fun onConnectionInfo(
socketUuid: String?,
macAddress: String?,
rokidAccount: String?,
glassesType: Int
) {
// 连接信息已在init阶段获取,此处可忽略
}
}
)
}
/**
* 权限请求结果回调(需在Activity中调用)
*/
fun onRequestPermissionsResult(grantResults: IntArray): Boolean {
val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
if (allGranted) {
checkBluetoothEnable()
return true
} else {
onInitFailed?.invoke("部分权限被拒绝,无法初始化蓝牙")
return false
}
}
/**
* 蓝牙开启请求结果回调(需在Activity中调用)
*/
fun onActivityResult(requestCode: Int, resultCode: Int): Boolean {
if (requestCode == REQUEST_ENABLE_BT) {
if (resultCode == Activity.RESULT_OK) {
scanRokidGlasses(BluetoothAdapter.getDefaultAdapter())
return true
} else {
onInitFailed?.invoke("用户拒绝开启蓝牙")
return false
}
}
return false
}
/**
* 反初始化蓝牙(应用退出时调用)
*/
fun deinit() {
CxrApi.getInstance().deinitBluetooth()
Log.d(TAG, "蓝牙反初始化完成")
}
}
核心代码解析
-
初始化流程逻辑:采用 “权限检查→蓝牙开启→设备扫描→SDK 初始化→连接建立” 的线性流程,确保每一步依赖前置条件满足。
-
关键函数说明:
-
startInit():初始化入口,触发整个流程; -
scanRokidGlasses():通过 UUID 过滤 Rokid 眼镜设备,优先检查已配对设备(减少扫描耗时); -
initSdkBluetooth():调用 SDK 核心接口CxrApi.initBluetooth(),通过BluetoothStatusCallback监听连接状态; -
connectToGlasses():基于onConnectionInfo回调的 UUID 和 MAC,调用CxrApi.connectBluetooth()完成最终连接。
-
2.3.2 步骤 2:眼镜端语音采集→手机端 ASR 识别
通过 SDK 的AudioStreamListener监听眼镜端录音数据流,将 PCM/Opus 格式的音频数据传输到手机端,再调用第三方 ASR 接口(如百度、讯飞)完成语音识别,最终得到文本结果。
import android.content.Context
import android.util.Log
import com.rokid.cxr.api.CxrApi
import com.rokid.cxr.callback.AudioStreamListener
import com.rokid.cxr.util.ValueUtil
class RokidVoiceCollector(
private val context: Context,
private val onAsrResult: ((text: String) -> Unit)? = null,
private val onError: ((errorMsg: String) -> Unit)? = null
) {
private val TAG = "RokidVoiceCollector"
// 录音流类型(与眼镜端AI助手关联)
private val STREAM_TYPE = "AI_assistant"
// 音频编码类型:1=PCM,2=Opus(此处选择PCM,便于ASR处理)
private val CODEC_TYPE = 1
// 第三方ASR客户端(需自行集成,此处以示例形式存在)
private val asrClient = ThirdPartyAsrClient(
onResult = { asrText -> onAsrResult?.invoke(asrText) },
onError = { errorMsg -> onError?.invoke(errorMsg) }
)
/**
* 启动语音采集(需在蓝牙连接成功后调用)
*/
fun startCollect(): Boolean {
// 1. 检查蓝牙连接状态
if (!CxrApi.getInstance().isBluetoothConnected) {
onError?.invoke("蓝牙未连接,无法启动语音采集")
return false
}
// 2. 设置音频流监听器(接收眼镜端录音数据)
CxrApi.getInstance().setAudioStreamListener(object : AudioStreamListener {
// 录音流开始回调
override fun onStartAudioStream(codecType: Int, streamType: String?) {
Log.d(TAG, "语音采集开始,编码类型:$codecType,流类型:$streamType")
// 初始化ASR客户端(传入音频参数:PCM、16kHz、单声道等)
asrClient.init(
sampleRate = 16000,
channelCount = 1,
bitDepth = 16,
codecType = codecType
)
}
// 录音数据流回调(核心:接收音频数据并传给ASR)
override fun onAudioStream(data: ByteArray?, offset: Int, length: Int) {
if (data == null || length <= 0) {
Log.w(TAG, "接收空音频数据,忽略")
return
}
Log.d(TAG, "接收音频数据:长度=$length 字节")
// 将音频数据传给ASR客户端(需截取有效数据:从offset开始,长度为length)
val validData = data.copyOfRange(offset, offset + length)
asrClient.sendAudioData(validData)
}
// 录音流结束回调(SDK未明确定义,需结合业务触发停止)
// 注:实际场景中可通过“长按眼镜AI键”或“手机端停止按钮”触发停止
})
// 3. 调用SDK接口开启录音
val status = CxrApi.getInstance().openAudioRecord(CODEC_TYPE, STREAM_TYPE)
return if (status == ValueUtil.CxrStatus.REQUEST_SUCCEED) {
Log.d(TAG, "语音采集启动成功")
true
} else {
val errorMsg = when (status) {
ValueUtil.CxrStatus.REQUEST_WAITING -> "录音请求等待中,请勿重复调用"
ValueUtil.CxrStatus.REQUEST_FAILED -> "录音请求失败"
else -> "未知状态:$status"
}
onError?.invoke(errorMsg)
false
}
}
/**
* 停止语音采集
*/
fun stopCollect() {
// 1. 停止ASR识别
asrClient.stop()
// 2. 调用SDK接口关闭录音
val status = CxrApi.getInstance().closeAudioRecord(STREAM_TYPE)
if (status == ValueUtil.CxrStatus.REQUEST_SUCCEED) {
Log.d(TAG, "语音采集停止成功")
} else {
Log.e(TAG, "语音采集停止失败,状态:$status")
}
// 3. 移除音频流监听器
CxrApi.getInstance().setAudioStreamListener(null)
}
/**
* 第三方ASR客户端(示例类,需替换为实际ASR SDK)
*/
private class ThirdPartyAsrClient(
private val onResult: ((text: String) -> Unit)?,
private val onError: ((errorMsg: String) -> Unit)?
) {
/**
* 初始化ASR(传入音频参数)
*/
fun init(sampleRate: Int, channelCount: Int, bitDepth: Int, codecType: Int) {
// 实际ASR初始化逻辑(如百度ASR的init方法)
Log.d("AsrClient", "初始化ASR:采样率=$sampleRate,声道数=$channelCount")
}
/**
* 发送音频数据给ASR
*/
fun sendAudioData(data: ByteArray) {
// 实际ASR发送数据逻辑(如百度ASR的send方法)
Log.d("AsrClient", "发送音频数据:长度=${data.size} 字节")
}
/**
* 停止ASR并获取结果
*/
fun stop() {
// 实际ASR停止逻辑,此处模拟返回识别结果
val mockResult = "打开提词器并显示欢迎文本" // 模拟ASR识别结果
onResult?.invoke(mockResult)
Log.d("AsrClient", "ASR识别完成,结果:$mockResult")
}
}
}
核心代码解析
-
核心流程:
-
启动采集:
startCollect()→检查蓝牙连接→设置AudioStreamListener→调用openAudioRecord()开启录音; -
数据传输:
onAudioStream()回调接收眼镜端音频数据→截取有效数据→传给第三方 ASR; -
停止采集:
stopCollect()→停止 ASR→调用closeAudioRecord()关闭录音→移除监听器。
-
-
关键参数说明:
-
STREAM_TYPE = "AI_assistant":与眼镜端 AI 助手服务关联,确保录音数据来自正确的音频流; -
CODEC_TYPE = 1:选择 PCM 编码(无压缩),避免 Opus 解码复杂度过高,适合 ASR 直接处理; -
asrClient:第三方 ASR 客户端示例,实际需集成百度、讯飞等 ASR SDK,传入正确的音频参数(采样率 16kHz、单声道、16bit 位深)。
-
2.3.3 步骤 3:语音指令下发→眼镜执行 + TTS 反馈
对 ASR 识别结果进行语义解析,提取指令类型(如 “打开提词器”“设置亮度”),调用 SDK 对应接口下发指令到眼镜执行;执行完成后,通过 SDK 的sendTtsContent()接口将 TTS 文本发送到眼镜,由眼镜端播放语音反馈。
import android.util.Log
import com.rokid.cxr.api.CxrApi
import com.rokid.cxr.util.ValueUtil
class RokidVoiceCommandExecutor(
private val onCommandExecuted: ((success: Boolean, feedback: String) -> Unit)? = null
) {
private val TAG = "RokidCommandExecutor"
// 提词器默认文本(Base64编码,实际需根据需求生成)
private val DEFAULT_WORD_TIPS_TEXT = "欢迎使用Rokid眼镜语音协同功能".toByteArray()
// 提词器文件名(用于SDK识别数据类型)
private val WORD_TIPS_FILE_NAME = "welcome_tips.txt"
/**
* 解析ASR结果并执行对应指令
*/
fun executeCommand(asrText: String) {
// 1. 检查蓝牙连接状态
if (!CxrApi.getInstance().isBluetoothConnected) {
val feedback = "蓝牙未连接,无法执行指令"
onCommandExecuted?.invoke(false, feedback)
sendTtsFeedback(feedback)
return
}
// 2. 语义解析:提取指令类型(实际需用NLP优化,此处简化匹配)
when {
asrText.contains("打开提词器") -> {
executeOpenWordTips(asrText)
}
asrText.contains("设置亮度") -> {
// 提取亮度值(如“设置亮度为10”)
val brightness = extractBrightness(asrText)
if (brightness != null) {
executeSetBrightness(brightness)
} else {
val feedback = "未识别到亮度值,请重新指令"
onCommandExecuted?.invoke(false, feedback)
sendTtsFeedback(feedback)
}
}
asrText.contains("关闭提词器") -> {
executeCloseWordTips()
}
else -> {
val feedback = "未识别指令:$asrText"
onCommandExecuted?.invoke(false, feedback)
sendTtsFeedback(feedback)
}
}
}
/**
* 执行“打开提词器”指令
*/
private fun executeOpenWordTips(asrText: String) {
// 步骤1:打开提词器场景
val openStatus = CxrApi.getInstance().controlScene(
sceneType = ValueUtil.CxrSceneType.WORD_TIPS,
openOrClose = true,
otherParams = null
)
if (openStatus != ValueUtil.CxrStatus.REQUEST_SUCCEED) {
val feedback = "打开提词器失败,状态:$openStatus"
onCommandExecuted?.invoke(false, feedback)
sendTtsFeedback(feedback)
return
}
// 步骤2:发送提词器文本(如从ASR中提取自定义文本,此处用默认文本)
val sendStatus = CxrApi.getInstance().sendStream(
type = ValueUtil.CxrStreamType.WORD_TIPS,
stream = DEFAULT_WORD_TIPS_TEXT,
fileName = WORD_TIPS_FILE_NAME,
cb = object : com.rokid.cxr.callback.SendStatusCallback {
override fun onSendSucceed() {
val feedback = "提词器已打开,显示欢迎文本"
onCommandExecuted?.invoke(true, feedback)
sendTtsFeedback(feedback)
}
override fun onSendFailed(errorCode: ValueUtil.CxrSendErrorCode?) {
val feedback = "提词器文本发送失败,错误码:$errorCode"
onCommandExecuted?.invoke(false, feedback)
sendTtsFeedback(feedback)
}
}
)
if (sendStatus != ValueUtil.CxrStatus.REQUEST_SUCCEED) {
val feedback = "提词器文本发送请求失败,状态:$sendStatus"
onCommandExecuted?.invoke(false, feedback)
sendTtsFeedback(feedback)
}
}
/**
* 执行“设置亮度”指令(亮度范围0-15)
*/
private fun executeSetBrightness(brightness: Int) {
if (brightness < 0 || brightness > 15) {
val feedback = "亮度值需在0-15之间,当前值:$brightness"
onCommandExecuted?.invoke(false, feedback)
sendTtsFeedback(feedback)
return
}
val status = CxrApi.getInstance().setGlassBrightness(brightness)
if (status == ValueUtil.CxrStatus.REQUEST_SUCCEED) {
val feedback = "亮度已设置为$brightness"
onCommandExecuted?.invoke(true, feedback)
sendTtsFeedback(feedback)
} else {
val feedback = "亮度设置失败,状态:$status"
onCommandExecuted?.invoke(false, feedback)
sendTtsFeedback(feedback)
}
}
/**
* 执行“关闭提词器”指令
*/
private fun executeCloseWordTips() {
val status = CxrApi.getInstance().controlScene(
sceneType = ValueUtil.CxrSceneType.WORD_TIPS,
openOrClose = false,
otherParams = null
)
if (status == ValueUtil.CxrStatus.REQUEST_SUCCEED) {
val feedback = "提词器已关闭"
onCommandExecuted?.invoke(true, feedback)
sendTtsFeedback(feedback)
} else {
val feedback = "提词器关闭失败,状态:$status"
onCommandExecuted?.invoke(false, feedback)
sendTtsFeedback(feedback)
}
}
/**
* 发送TTS反馈到眼镜端(由眼镜播放语音)
*/
private fun sendTtsFeedback(feedbackText: String) {
Log.d(TAG, "发送TTS反馈:$feedbackText")
val status = CxrApi.getInstance().sendTtsContent(feedbackText)
if (status != ValueUtil.CxrStatus.REQUEST_SUCCEED) {
Log.e(TAG, "TTS反馈发送失败,状态:$status")
} else {
// 通知眼镜TTS播放结束(可选,根据业务需求)
CxrApi.getInstance().notifyTtsAudioFinished()
}
}
/**
* 从ASR文本中提取亮度值(简单正则匹配)
*/
private fun extractBrightness(asrText: String): Int? {
val regex = Regex("亮度(为|设置为)?(\d+)")
val matchResult = regex.find(asrText)
return matchResult?.groupValues?.get(2)?.toIntOrNull()
}
}
核心代码解析
-
执行流程:
-
语义解析:
executeCommand()通过关键词匹配(如 “打开提词器”“设置亮度”)提取指令类型; -
指令执行:
-
打开提词器:
executeOpenWordTips()→调用controlScene()打开场景→sendStream()发送提词器文本; -
设置亮度:
executeSetBrightness()→提取亮度值(0-15)→调用setGlassBrightness()设置亮度;
-
-
TTS 反馈:
sendTtsFeedback()→调用sendTtsContent()将反馈文本发送到眼镜,由眼镜端播放语音。
-
-
关键接口说明:
-
controlScene():控制眼镜场景(打开 / 关闭提词器、录像等),sceneType需指定为ValueUtil.CxrSceneType.WORD_TIPS; -
sendStream():发送提词器文本数据,type需指定为ValueUtil.CxrStreamType.WORD_TIPS,确保眼镜端识别为提词器内容; -
sendTtsContent():发送 TTS 文本到眼镜,支持中文语音播放,需配合notifyTtsAudioFinished()通知播放结束。
-
2.4 核心总结
-
初始化阶段:手机通过蓝牙扫描并连接 Rokid 眼镜,完成 SDK 初始化,为后续数据传输奠定基础;
-
语音采集与识别阶段:眼镜端启动录音,通过
AudioStreamListener将音频数据传输到手机端,第三方 ASR 将音频转为文本; -
指令执行与反馈阶段:解析 ASR 文本得到指令,调用 SDK 接口下发到眼镜执行,同时通过 TTS 将执行结果反馈给用户。
-
实用价值:
-
解放双手:用户无需操作手机,通过语音指令控制眼镜(如打开提词器、拍照、设置亮度),适合工业巡检、医疗会诊等场景;
-
远程协同:手机端可实时获取眼镜端音频数据,结合 ASR 和 TTS 实现远程指导(如工程师通过语音指导现场人员操作)。
-
四、总结与展望
Rokid CXR-M SDK为开发者提供了丰富而强大的功能,使得手机与AR眼镜之间的协同交互变得前所未有的便捷与高效。通过深入了解SDK的核心技术原理与功能,开发者能够更好地利用这些功能,开发出满足用户需求的AR应用。未来,随着AR技术的不断发展和普及,Rokid CXR-M SDK将继续迭代优化,为开发者提供更多元数据通信、实时音视频处理等高级功能,进一步拓展AR眼镜在工业巡检、远程协助等场景中的应用。
同时,期待Rokid CXR-M SDK能够在未来支持更多设备类型、提供更丰富的API接口以及更广泛的开发者工具集,助力AR眼镜在工业领域发挥更大的价值。