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

325 阅读14分钟

【征文计划】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 需蓝牙先连,双通道均有专属回调监听状态。

核心代码示例

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

核心代码解析

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

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

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眼镜在工业领域发挥更大的价值。