打造无缝演讲体验:基于 Rokid AR 眼镜与 CXR-M SDK 的智能提词解决方案

148 阅读3分钟

技术选型与整体架构

核心技术栈

  • 移动端:Kotlin + Android Jetpack(ViewModel, LiveData)
  • SDK:Rokid CXR-M SDK v1.0.1-Preview(仅支持 Android)
  • 通信协议:蓝牙(控制信令) + Wi-Fi P2P(媒体同步)
  • UI 渲染:眼镜端使用 SDK 内置的「自定义页面场景」(JSON 驱动)

系统架构图

整个系统以 手机为控制中枢,眼镜为 显示与交互终端。CXR-M SDK 作为桥梁,封装了底层通信细节,让我们能专注于业务逻辑。

项目初始化:集成 CXR-M SDK

配置 Maven 仓库

settings.gradle.kts 中添加 Rokid 私有仓库:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        maven { url = uri("https://maven.rokid.com/repository/maven-public/") }
        google()
        mavenCentral()
    }
}

添加依赖与权限

build.gradle.kts 中:

android {
    defaultConfig {
        minSdk = 28 // 必须 ≥28
    }
}

dependencies {
    implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2")
    // 其他 transitive 依赖(如 Retrofit, OkHttp)SDK 已声明,无需重复添加
}

AndroidManifest.xml 中声明权限(缺一不可):

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />

⚠️ 注意:从 Android 12(API 31)起,蓝牙扫描/连接需动态申请 BLUETOOTH_SCANBLUETOOTH_CONNECT,且必须搭配 ACCESS_FINE_LOCATION

权限动态申请封装

我封装了一个 PermissionHelper,在 MainActivity 启动时调用:

class PermissionHelper(private val activity: AppCompatActivity) {
    fun requestRequiredPermissions(onGranted: () -> Unit) {
        val permissions = mutableListOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.BLUETOOTH,
            Manifest.permission.BLUETOOTH_ADMIN
        )
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            permissions += Manifest.permission.BLUETOOTH_SCAN
            permissions += Manifest.permission.BLUETOOTH_CONNECT
        }

        if (permissions.all { activity.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED }) {
            onGranted()
        } else {
            activity.requestPermissions(permissions.toTypedArray(), REQUEST_CODE)
        }
    }
}

只有权限全部授予,才进入设备连接流程。


设备连接:蓝牙 + Wi-Fi 双通道建立

蓝牙连接流程

CXR-M SDK 的蓝牙连接分为两步:初始化连接

// 1. 初始化蓝牙(传入扫描到的 BluetoothDevice)
CxrApi.getInstance().initBluetooth(context, device, object : BluetoothStatusCallback {
    override fun onConnectionInfo(socketUuid, macAddress, rokidAccount, glassesType) {
        // 保存 socketUuid 和 macAddress,用于后续连接
        connectBluetooth(socketUuid!!, macAddress!!)
    }
    override fun onConnected() { /* 连接成功 */ }
    override fun onDisconnected() { /* 断开处理 */ }
    override fun onFailed(errorCode) { /* 错误处理 */ }
})

// 2. 正式连接
private fun connectBluetooth(uuid: String, mac: String) {
    CxrApi.getInstance().connectBluetooth(context, uuid, mac, callback)
}

💡 提示:socketUuid 是 Rokid Glasses 在 BLE 广播中携带的自定义服务 UUID(00009100-...),用于建立 RFCOMM 通道。

Wi-Fi P2P 初始化(用于后续文件同步)

提词内容虽小,但若未来扩展图片/视频提词,则需 Wi-Fi 通道。初始化如下:

val wifiStatus = CxrApi.getInstance().initWifiP2P(object : WifiP2PStatusCallback {
    override fun onConnected() {
        Log.d("WiFi", "P2P connected")
    }
    override fun onDisconnected() { /* ... */ }
    override fun onFailed(errorCode) { /* ... */ }
})

核心功能实现:提词器场景构建

打开提词器场景

fun openWordTips() {
    CxrApi.getInstance().controlScene(
        ValueUtil.CxrSceneType.WORD_TIPS,
        true, // true = 打开
        null
    )
}

发送提词内容

提词内容以 ByteArray 形式通过 sendStream 发送:

fun sendScript(text: String, fileName: String = "script.txt") {
    CxrApi.getInstance().sendStream(
        ValueUtil.CxrStreamType.WORD_TIPS,
        text.toByteArray(),
        fileName,
        object : SendStatusCallback {
            override fun onSendSucceed() {
                Log.d("Script", "Sent successfully")
            }
            override fun onSendFailed(errorCode) {
                Log.e("Script", "Send failed: $errorCode")
            }
        }
    )
}

配置提词器显示参数

支持字体大小、行距、显示区域、滚动模式等:

fun configureWordTips() {
    CxrApi.getInstance().configWordTipsText(
        textSize = 18f,      // 字体大小(sp)
        lineSpace = 1.5f,    // 行距
        mode = "ai",         // "ai" 模式支持 ASR 自动滚动
        startPointX = 100,   // X 起始坐标(像素)
        startPointY = 200,   // Y 起始坐标
        width = 800,         // 显示区域宽
        height = 600         // 显示区域高
    )
}

AI 模式下的 ASR 联动

当用户说话时,手机端 ASR 引擎识别结果,实时发送给眼镜,触发自动滚动:

fun onAsrResult(text: String) {
    CxrApi.getInstance().sendAsrContent(text) // 注意:此处复用 AI 场景的 ASR 接口
}

关键点:提词器的 "ai" 模式会监听 ASR 内容,当识别文本接近当前显示末尾时,自动向上滚动,确保用户始终看到下一句。


增强体验:设备状态控制与监听

远程调节亮度/音量

演讲环境光线多变,允许用户在手机端滑动调节眼镜亮度:

// 设置监听器(获取当前亮度)
CxrApi.getInstance().setBrightnessUpdateListener { brightness ->
    viewModel.updateBrightnessUI(brightness)
}

// 设置亮度(0-15)
CxrApi.getInstance().setGlassBrightness(10)

同理,音量控制也支持双向同步。

电量与充电状态监听

CxrApi.getInstance().setBatteryLevelUpdateListener { level, charging ->
    Log.d("Battery", "Level: $level%, Charging: $charging")
    if (level < 20 && !charging) {
        showLowBatteryWarning()
    }
}

自动关机与熄屏策略

为节省电量,设置 10 分钟无操作自动关机:

CxrApi.getInstance().setPowerOffTimeout(10) // 单位:分钟
CxrApi.getInstance().setScreenOffTimeout(60) // 60秒无操作熄屏

异常处理与健壮性设计

连接断开重连机制

监听 onDisconnected(),尝试自动重连:

override fun onDisconnected() {
    viewModel.setConnectionStatus(false)
    // 延迟 3 秒后重试
    handler.postDelayed({
        if (lastMac != null && lastUuid != null) {
            connectBluetooth(lastUuid!!, lastMac!!)
        }
    }, 3000)
}

权限缺失友好提示

若用户拒绝关键权限,引导其手动开启:

if (!allGranted) {
    AlertDialog.Builder(this)
        .setTitle("权限不足")
        .setMessage("请授予位置和蓝牙权限,否则无法连接眼镜")
        .setPositiveButton("去设置") { _, _ ->
            val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
            intent.data = Uri.parse("package:$packageName")
            startActivity(intent)
        }
        .show()
}

SDK 错误码解析

CXR-M SDK 定义了详细的错误码(如 CxrBluetoothErrorCode.SOCKET_CONNECT_FAILED),我在日志中统一映射为用户可读信息:

when (errorCode) {
    ValueUtil.CxrBluetoothErrorCode.BLE_CONNECT_FAILED -> "蓝牙连接失败,请靠近眼镜重试"
    ValueUtil.CxrWifiErrorCode.WIFI_DISABLED -> "请先打开手机 Wi-Fi"
    else -> "未知错误,请重启应用"
}

性能优化与用户体验细节

避免频繁发送相同内容

在发送提词脚本前,做内容哈希比对,避免重复传输:

if (currentScriptHash != newScript.hashCode()) {
    sendScript(newScript)
    currentScriptHash = newScript.hashCode()
}

Wi-Fi P2P 按需开启

仅在用户点击“同步历史图片”时才初始化 Wi-Fi,用完立即 deinitWifiP2P()

自定义页面作为备选方案

若未来提词需求复杂(如图文混排),可使用「自定义页面场景」:

{
  "type": "LinearLayout",
  "props": { "orientation": "vertical" },
  "children": [
    { "type": "TextView", "props": { "text": "标题", "textSize": "20sp" } },
    { "type": "ImageView", "props": { "name": "icon1" } }
  ]
}

先调用 sendCustomViewIcons() 上传图标,再 openCustomView(json) 显示。

总结

本项目成功基于Rokid CXR-M SDK,构建了一个以手机为控制中枢、AR眼镜为显示终端的提词器应用。通过蓝牙与Wi-Fi P2P的双通道稳定通信,我们不仅实现了提词内容的实时推送与AI语音联动滚动,还完成了设备状态监控、远程调节等增强功能。整个架构通过模块化设计与全面的异常处理机制,确保了应用的流畅性、稳定性与良好的用户体验,为AR在演讲、直播等场景的应用提供了可靠的实践方案。