【征文计划】Rokid CXR-M SDK全解析:从设备连接到语音交互的AR协同开发指南

【征文计划】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都能提供全面的支持。

主要功能特性

  1. 设备连接与管理:支持通过蓝牙和Wi-Fi与Rokid Glasses建立稳定连接,并获取设备状态信息。

  2. 自定义场景交互:快速接入YodaOS-Sprite操作系统定义的场景交互流程,实现自定义功能开发。

  3. AI助手服务:高效利用Rokid Assist Service中的服务,包括文件互传、录音、拍照等功能。

  4. 实时音视频传输:支持眼镜端音视频数据的实时采集与传输至手机端进行处理。

  5. 数据同步与媒体管理:支持眼镜端与手机端之间的媒体文件同步与管理。

二、核心技术原理

1. 设备连接机制

以蓝牙为基础控制通道、Wi-Fi P2P 为高带宽补充,蓝牙按 "扫描→初始化→连接" 流程,用 UUID 过滤设备,Wi-Fi 需蓝牙先连,双通道均有专属回调监听状态。

核心代码示例

kotlin 复制代码
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,先存后传。按带宽分层传输,用回调控延迟,适配不同场景需求。

核心代码示例

kotlin 复制代码
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()启停,专属接口配参,回调监状态。

核心代码示例

kotlin 复制代码
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 在线管理,需按官方文档配置仓库与依赖:

kotlin 复制代码
// 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 复制代码
<?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 动态申请权限

kotlin 复制代码
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 过滤)→初始化蓝牙→建立连接" 的官方流程执行,适配工业场景 "一次配对、多次复用" 需求(减少现场重复操作)。

kotlin 复制代码
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, "蓝牙反初始化完成")
    }
}

核心代码解析

  1. 初始化流程逻辑:采用 "权限检查→蓝牙开启→设备扫描→SDK 初始化→连接建立" 的线性流程,确保每一步依赖前置条件满足。

  2. 关键函数说明

    • 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 接口(如百度、讯飞)完成语音识别,最终得到文本结果。

kotlin 复制代码
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")
        }
    }
}

核心代码解析

  1. 核心流程

    • 启动采集:startCollect()→检查蓝牙连接→设置AudioStreamListener→调用openAudioRecord()开启录音;

    • 数据传输:onAudioStream()回调接收眼镜端音频数据→截取有效数据→传给第三方 ASR;

    • 停止采集:stopCollect()→停止 ASR→调用closeAudioRecord()关闭录音→移除监听器。

  2. 关键参数说明

    • 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 文本发送到眼镜,由眼镜端播放语音反馈。

kotlin 复制代码
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()
    }
}
核心代码解析
  1. 执行流程

    • 语义解析:executeCommand()通过关键词匹配(如 "打开提词器""设置亮度")提取指令类型;

    • 指令执行:

      • 打开提词器:executeOpenWordTips()→调用controlScene()打开场景→sendStream()发送提词器文本;

      • 设置亮度:executeSetBrightness()→提取亮度值(0-15)→调用setGlassBrightness()设置亮度;

    • TTS 反馈:sendTtsFeedback()→调用sendTtsContent()将反馈文本发送到眼镜,由眼镜端播放语音。

  2. 关键接口说明

    • controlScene():控制眼镜场景(打开 / 关闭提词器、录像等),sceneType需指定为ValueUtil.CxrSceneType.WORD_TIPS

    • sendStream():发送提词器文本数据,type需指定为ValueUtil.CxrStreamType.WORD_TIPS,确保眼镜端识别为提词器内容;

    • sendTtsContent():发送 TTS 文本到眼镜,支持中文语音播放,需配合notifyTtsAudioFinished()通知播放结束。

2.4 核心总结

  1. 初始化阶段:手机通过蓝牙扫描并连接 Rokid 眼镜,完成 SDK 初始化,为后续数据传输奠定基础;

  2. 语音采集与识别阶段 :眼镜端启动录音,通过AudioStreamListener将音频数据传输到手机端,第三方 ASR 将音频转为文本;

  3. 指令执行与反馈阶段:解析 ASR 文本得到指令,调用 SDK 接口下发到眼镜执行,同时通过 TTS 将执行结果反馈给用户。

  4. 实用价值

    • 解放双手:用户无需操作手机,通过语音指令控制眼镜(如打开提词器、拍照、设置亮度),适合工业巡检、医疗会诊等场景;

    • 远程协同:手机端可实时获取眼镜端音频数据,结合 ASR 和 TTS 实现远程指导(如工程师通过语音指导现场人员操作)。

四、总结与展望

Rokid CXR-M SDK为开发者提供了丰富而强大的功能,使得手机与AR眼镜之间的协同交互变得前所未有的便捷与高效。通过深入了解SDK的核心技术原理与功能,开发者能够更好地利用这些功能,开发出满足用户需求的AR应用。未来,随着AR技术的不断发展和普及,Rokid CXR-M SDK将继续迭代优化,为开发者提供更多元数据通信、实时音视频处理等高级功能,进一步拓展AR眼镜在工业巡检、远程协助等场景中的应用。

同时,期待Rokid CXR-M SDK能够在未来支持更多设备类型、提供更丰富的API接口以及更广泛的开发者工具集,助力AR眼镜在工业领域发挥更大的价值。

相关推荐
合作小小程序员小小店3 小时前
web开发,学院培养计划系统,基于Python,FlaskWeb,Mysql数据库
后端·python·mysql·django·web app
笃行3503 小时前
基于Rokid CXR-S SDK的智能AR翻译助手技术拆解与实现指南
后端
文心快码BaiduComate3 小时前
代码·创想·未来——百度文心快码创意探索Meetup来啦
前端·后端·程序员
渣哥3 小时前
面试官最爱刁难:Spring 框架里到底用了多少经典设计模式?
javascript·后端·面试
疯狂的程序猴4 小时前
iOS混淆实战全解析,从源码混淆到IPA文件加密,打造苹果应用反编译防护体系
后端
开心就好20254 小时前
iOS 26 文件管理实战,多工具组合下的 App 数据访问与系统日志调试方案
后端
乘风破浪酱524364 小时前
PO、DTO、VO的区别与应用场景详解
后端
盖世英雄酱581365 小时前
分库分表正在被淘汰
数据库·后端
间彧5 小时前
CountDownLatch详解与项目实战
后端