欢迎光临
我们一直在努力

Rokid 智能眼镜开发实战:手机端 + 眼镜端双端协同 App 完整指南

本文基于 Rokid CXR-M SDK(手机端)和CXR-L SDK(眼镜端),从零讲解如何开发一个手机-眼镜双端协同应用,涵盖蓝牙扫描配对、眼镜连接、远程拍照、媒体同步、自定义视图推送、HID 遥控等核心功能。

一、项目概览

本项目包含两个独立 APK,分别运行在手机和 Rokid 智能眼镜上:

模块

SDK         运行平台 核心能力
APP CXR-M SDK v1.0.9

Android

蓝牙扫描配对、连接眼镜、远程拍照、Wi-Fi P2P 媒体同步、自定义视图推送

Glasses         CXR-L SDK v1.0.1 Android 

蓝牙 RFCOMM 接收指令、CXR 服务绑定、屏幕显示

技术栈:Kotlin 2.1.0、Android SDK 36 、Gradle 8.11.1、GreenDAO、Retrofit+OkHttp、Glide 、RxJava3、EventBus、QMUI

二、环境配置

2.1 仓库配置

Rokid SDK 托管在私有 Maven 仓库,需在settings.gradle中添加:

dependencyResolutionManagement {
repositories {
maven { url 'https://maven.aliyun.com/repository/public/' }
maven { url 'https://maven.aliyun.com/repository/google/' }
maven { url = uri("https://maven.rokid.com/repository/maven-public/") }
google()
mavenCentral()
}
}

2.2 SDK 依赖

在gradle/libs.versions.toml`中声明版本:

[versions]
cxr-m-version = "1.0.9"
cxr-l-version = "1.0.1"

[libraries]
rokid-cxr-m = { module = "com.rokid.cxr:client-m", version.ref = "cxr-m-version" }
rokid-cxr-l = { module = "com.rokid.cxr:client-l", version.ref = "cxr-l-version" }

手机端app/build.gradle:

implementation(libs.rokid.cxr.m) {
exclude group: 'com.rokid.cxr', module: 'client-m-sources'
}

眼镜端 `glasses/build.gradle`:

implementation fileTree(dir: 'libs', include: ['*.jar'])

2.3 鉴权配置

手机端需要.lc`鉴权文件和 CLIENT_SECRET:

1.lc鉴权文件和SECRET的获取方式:

https://account-web.rokid.com/

注意:实名认证和开发者认证都需要通过后才可进行下一步

进入凭证信息中,查看客户端密钥,里面有等会需要填写的CLENT_SECRET

还需要回到首页的控制台Rokid AR Platform进入设备管理,SN可以从设备包装盒或手机APP中查看。目前只支持绑定设备,不支持删除或修改设备。

点击下载鉴权文件即可下载.lc文件

2. 将 .lc文件放到app/src/main/res/raw/目录

3. 在CommonModel.kt中配置密钥:

object CommonModel {
const val CLIENT_SECRET = "your-client-secret"
}

三、核心功能实现

3.1 蓝牙扫描与配对

手机端通过 BLE 扫描发现 Rokid 眼镜设备,使用 CXR-M SDK 的 Service UUID 过滤:

class BluetoothScanActivity : BaseActivity<ActivityBluetoothScanBinding>(…) {

private fun startScan() {

val filter = ScanFilter.Builder()
.setServiceUuid(ParcelUuid(UUID.fromString("00009100-0000-1000-8000-00805f9b34fb")))
.build()
val settings = ScanSettings.Builder().build()
bluetoothLeScanner?.startScan(listOf(filter), settings, scanCallback)
}

private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
result?.let {
val device = BluetoothDevice(
device = it.device,
name = it.device.name,
address = it.device.address,
rssi = it.rssi
)
deviceAdapter.addDevice(device)
}
}
}
}

 注意:Android 12+ 需要申请BLUETOOTH_SCAN和BLUETOOTH_CONNECT权限,低于 Android 12 使用BLUETOOTH和BLUETOOTH_ADMIN。

3.2 连接眼镜(CXR-M SDK 核心)

点击扫描到的设备后,通过CxrApi建立连接,整个流程分两步:

第一步:BLE初始化→获取 Socket UUID 和 MAC 地址

第二步:Socket连接→建立数据通道

object CxrUtil {

fun initConnectDevice(
context: Context,
device: BluetoothDevice,
listener: BluetoothDeviceConnectListener,
) {
// 第一步:BLE 初始化,获取连接信息
CxrApi.getInstance().initBluetooth(context, device, object : BluetoothStatusCallback {
override fun onConnectionInfo(
socketUuid: String?, macAddress: String?,
rokidAccount: String?, glassesType: Int,
) {
// 拿到 socketUuid 和 macAddress,进入第二步
socketUuid?.let { uuid ->
macAddress?.let { address ->
connectGlasses(context, device.name, uuid, address) { state ->
if (state == GlassesLinkState.CONNECTED) {
// 保存设备信息到 SharedPreferences
SPUtils.getInstance().put(ConsModel.SP_KEY_SOCKET_UUID, uuid)
SPUtils.getInstance().put(ConsModel.SP_KEY_MAC_ADDRESS, address)
}
listener.onDeviceConnected(state, CommonModel.glassesLinkErrorMsg)
}
}
}
}
override fun onConnected() { listener.onBluetoothConnect(BluetoothLinkState.CONNECTED) }
override fun onDisconnected() { listener.onBluetoothConnect(BluetoothLinkState.DISCONNECTED) }
override fun onFailed(p0: ValueUtil.CxrBluetoothErrorCode?) {
listener.onBluetoothConnect(BluetoothLinkState.FAILED, errorMsg)
}
})
}

// 第二步:使用 Socket UUID + MAC + 鉴权信息建立连接
fun connectGlasses(
context: Context, deviceName: String,
socketUuid: String, macAddress: String,
action: (GlassesLinkState) -> Unit,
) {
CxrApi.getInstance().connectBluetooth(
context, socketUuid, macAddress,
object : BluetoothStatusCallback { /* 回调处理 */ },
readRawFile(context),
CommonModel.CLIENT_SECRET
)
initGlassesListener()
}
}

连接状态流转:

UNPAIRED → BLE扫描 → UNCONNECTED → initBluetooth → ON_CONNECTION_INFO
→ connectBluetooth → CONNECTING → CONNECTED

已配对设备快速重连:

fun initGlassesLink(appContext: Context, action: (GlassesLinkState) -> Unit) {
if (CommonModel.deviceMacAddress.isNotEmpty()) {
connectGlasses(appContext, CommonModel.deviceDeviceName,
CommonModel.deviceSocketUuid, CommonModel.deviceMacAddress, action)
}
}

3.3 远程拍照

CXR-M SDK 提供两种拍照方式:

API 返回数据 适用场景

takeGlassPhotoGlobal()

字节数组 (ByteArray)

AI 识别、即时处理

takeGlassPhoto()

文件路径 (String)

保存到眼镜本地

推荐使用 takeGlassPhotoGlobal(),直接获取图片字节数据:

class HomeVm(val appContext: Application) : AndroidViewModel(appContext) {

val cameraSize = Size(1024, 768) // 眼镜摄像头旋转90°,宽高互换

fun takePhotoGlobal(size: Size) {
when (val status = CxrApi.getInstance().takeGlassPhotoGlobal(
size.width, size.height, 100, pictureCallback
)) {
ValueUtil.CxrStatus.REQUEST_SUCCEED -> { /* 请求已发送 */ }
ValueUtil.CxrStatus.REQUEST_WAITING -> { /* 眼镜忙,稍后重试 */ }
ValueUtil.CxrStatus.REQUEST_FAILED -> { /* 请求失败 */ }
else -> { /* 异常 */ }
}
}

private val pictureCallback = PhotoResultCallback { status, imageData ->
when (status) {
ValueUtil.CxrStatus.RESPONSE_SUCCEED -> {
if (imageData.isNullOrEmpty()) return@PhotoResultCallback
// 保存图片到本地
val imagePath = AppInternalFileUtil.createRecordFile(
"rokid_${AppTimeUtils.getCurrentTime()}.webp"
)
File(imagePath).writeBytes(imageData)
// 写入数据库记录
writeUpdateRecord(GlassesRecordModel().apply {
direction = InteractionDirection.FROM_DEVICE.value
content = imagePath
message = "眼镜拍照 WebP"
})
}
else -> { /* 拍照失败 */ }
}
}
}

踩坑提醒:眼镜摄像头物理方向旋转了 90°,拍照时 width/height 需要互换。比如想要 768×1024 的照片,应传入 Size(1024, 768)。

3.4 Wi-Fi P2P 媒体同步

眼镜端照片/视频通过 Wi-Fi P2P 传输到手机,流程如下:

initWifiP2P → P2P连接成功 → startSync → 逐文件同步回调 → onSyncFinished → deinitWifiP2P
// 1. 建立 Wi-Fi P2P 连接
CxrApi.getInstance().initWifiP2P(object : WifiP2PStatusCallback {
override fun onConnected() {
// 2. P2P 连接成功,开始同步
startSync(mediaTypes, saveDir)
}
override fun onDisconnected() { /* 断开 */ }
override fun onFailed(errorCode: ValueUtil.CxrWifiErrorCode?) { /* 失败 */ }
})

// 3. 启动同步
fun startSync(mediaTypes: Array<ValueUtil.CxrMediaType>, saveDir: File) {
val savePath = saveDir.absolutePath + File.separator
CxrApi.getInstance().startSync(savePath, mediaTypes, object : SyncStatusCallback {
override fun onSyncStart() { /* 同步开始 */ }
override fun onSingleFileSynced(fileName: String?) {
// 单个文件同步完成,fileName 是保存到本地的完整路径
val file = resolveSyncedFile(fileName)
writeSyncedMediaRecord(file)
}
override fun onSyncFailed() { disconnectWifiP2P() }
override fun onSyncFinished() { disconnectWifiP2P() }
})
}

// 查询未同步数量
CxrApi.getInstance().getUnsyncNum { status, audioNum, pictureNum, videoNum ->
// 获取各类型未同步文件数
}

// 停止同步
CxrApi.getInstance().stopSync()
CxrApi.getInstance().deinitWifiP2P()

同步媒体类型:

// 仅同步照片
syncPicturesOnly() // arrayOf(ValueUtil.CxrMediaType.PICTURE)

// 仅同步视频
syncVideosOnly() // arrayOf(ValueUtil.CxrMediaType.VIDEO)

// 同步照片和视频
syncPicturesAndVideos() // arrayOf(ValueUtil.CxrMediaType.PICTURE, ValueUtil.CxrMediaType.VIDEO)

3.5 自定义视图推送(在眼镜上显示 )

CXR-M SDK 支持通过 JSON 描述在眼镜屏幕上渲染自定义视图:

fun showCustomView(text: String) {
val customViewMap = mapOf(
"type" to "LinearLayout",
"props" to mapOf(
"id" to "main",
"layout_width" to "match_parent",
"layout_height" to "match_parent",
"orientation" to "vertical",
"gravity" to "center_vertical",
"backgroundColor" to "#FF000000"
),
"children" to listOf(
mapOf(
"type" to "TextView",
"props" to mapOf(
"text" to text,
"textSize" to "16sp",
"textStyle" to "bold",
"textColor" to "#FFFFFFFF"
)
)
)
)
// 打开自定义视图
CxrApi.getInstance().openCustomView(Gson().toJson(customViewMap))

// 更新视图
CxrApi.getInstance().updateCustomView(updatedJson)

// 关闭视图
CxrApi.getInstance().closeCustomView()
}

支持的视图类型:LinearLayout、RelativeLayout、TextView、ImageView。

视图生命周期回调:

CxrApi.getInstance().setCustomViewListener(object : CustomViewListener {
override fun onIconsSent() { /* 图标已发送 */ }
override fun onOpened() { /* 视图已打开 */ }
override fun onOpenFailed(p0: Int) { /* 打开失败 */ }
override fun onUpdated() { /* 视图已更新 */ }
override fun onClosed() { /* 视图已关闭 */ }
})

3.6 眼镜端 APP 开发

眼镜端 APP 有两种通信方式:

方式一:蓝牙 RFCOMM(自定义通道)

手机端作为客户端,眼镜端作为服务端,通过 RFCOMM 传输 JSON 消息。

眼镜端 — BluetoothServer:

class BluetoothServer(private val listener: OnMessageListener) {
companion object {
val SERVICE_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
}

fun start(): Boolean {
val adapter = BluetoothAdapter.getDefaultAdapter()
serverSocket = adapter.listenUsingRfcommWithServiceRecord("GlassesTextServer", SERVICE_UUID)
running = true
acceptThread = Thread { acceptLoop() }
acceptThread?.start()
return true
}

private fun readLoop(socket: BluetoothSocket) {
val input = socket.inputStream
val buffer = ByteArray(4096)
val stringBuilder = StringBuilder()

while (running && socket.isConnected) {
val bytesRead = input.read(buffer)
if (bytesRead == -1) break
stringBuilder.append(String(buffer, 0, bytesRead, Charsets.UTF_8))

// 按换行符分割 JSON 消息
var newlineIdx: Int
while (stringBuilder.indexOf('\\n').also { newlineIdx = it } >= 0) {
val line = stringBuilder.substring(0, newlineIdx).trim()
stringBuilder.delete(0, newlineIdx + 1)
if (line.isNotEmpty()) handleMessage(line)
}
}
}

private fun handleMessage(line: String) {
val json = JSONObject(line)
val type = json.optString("type", "") // "show_text" / "close_text"
val text = json.optString("text", "")
listener.onMessageReceived(type, text)
}
}

手机端 — BluetoothClient:

object BluetoothClient {
val SERVICE_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")

fun connect(macAddress: String): Boolean {
val device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress)
socket = device.createRfcommSocketToServiceRecord(SERVICE_UUID)
socket?.connect()
outputStream = socket?.outputStream
return true
}

fun sendText(text: String): Boolean = sendMessage("show_text", text)
fun sendClose(): Boolean = sendMessage("close_text", "")

private fun sendMessage(type: String, text: String): Boolean {
val json = JSONObject().apply { put("type", type); put("text", text) }
val data = (json.toString() + "\\n").toByteArray(Charsets.UTF_8)
outputStream?.write(data)
outputStream?.flush()
return true
}
}

方式二:CXR 服务绑定(AIDL)

眼镜端绑定 Rokid 系统服务 `IMediaStreamService`,接收手机端通过 `sendCustomCmd` 发送的指令:

class CxrServiceManager(private val context: Context) {

private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
mediaStreamService = IMediaStreamService.Stub.asInterface(service)
registerCustomCmdCallback()
}
override fun onServiceDisconnected(name: ComponentName?) {
mediaStreamService = null
}
}

fun connect(): Boolean {
val intent = Intent("com.rokid.sprite.aiapp.externalapp.MEDIA_STREAM_SERVICE").apply {
setPackage("com.rokid.cxrservice")
putExtra("auth_token", "glasses_app_token")
}
return context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}

private val customCmdCallback = object : ICustomCmdCallback.Stub() {
override fun onCustomCmdResult(cmd: String?, data: ByteArray?) {
val caps = Caps.fromBytes(data)
when (cmd) {
"show_text" -> {
val text = caps.a[0].getString()
messageListener?.onMessageReceived(text)
}
"close_text" -> messageListener?.onMessageReceived("")
}
}
}

private fun registerCustomCmdCallback(): Boolean {
return mediaStreamService?.registerCustomCmdCallback(customCmdCallback) ?: false
}
}

3.7 设备状态监听

连接成功后,可监听眼镜的音量、亮度、电量变化:

fun initGlassesListener() {
CxrApi.getInstance().setVolumeUpdateListener { volume ->
CommonModel.glassesInfo.volume = volume
}
CxrApi.getInstance().setBrightnessUpdateListener { brightness ->
CommonModel.glassesInfo.brightness = brightness
}
CxrApi.getInstance().setBatteryLevelUpdateListener { batteryLevel, charging ->
CommonModel.glassesInfo.battery = batteryLevel
CommonModel.glassesInfo.charging = charging
}
}

// 主动设置音量/亮度(范围 0-15)
CxrApi.getInstance().setGlassVolume(volume)
CxrApi.getInstance().setGlassBrightness(brightness)

3.8 录像控制

fun controlVideoRecord(toOpen: Boolean) {
CxrApi.getInstance().controlScene(
ValueUtil.CxrSceneType.VIDEO_RECORD,
toOpen, // true=开始录像, false=停止录像
null
)
}

3.9 AI 场景(语音触发拍照)

fun initAiScene() {
// 监听眼镜端语音流
CxrApi.getInstance().setAudioStreamListener(object : AudioStreamListener {
override fun onStartAudioStream(p0: Int, p1: Int, p2: String?) {
// 语音开始 → 自动触发拍照
takePhotoGlobal(cameraSize)
}
override fun onAudioStream(p0: Int, p1: ByteArray?, p2: Int, p3: Int) { /* 音频数据 */ }
override fun onAudioStreamFinish(p0: Int) { /* 语音结束 */ }
})

// 监听 AI 按键事件
CxrApi.getInstance().setAiEventListener(object : AiEventListener {
override fun onAiKeyDown() { /* AI 按键按下 */ }
override fun onAiKeyUp() { /* AI 按键抬起 */ }
override fun onAiExit() { /* AI 退出 */ }
})
}

// 识别结果反馈到眼镜端(TTS 播报)
CxrApi.getInstance().sendGlobalMsgContent(0, feedbackText, false)

四、权限清单

手机端 `AndroidManifest.xml` 需要的权限:

<!– 蓝牙 –>
<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" tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!– 位置(BLE 扫描需要) –>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<!– 存储 –>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<!– 网络(Wi-Fi P2P) –>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />

<!– 其他 –>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

五、踩坑总结

问题 原因 解决方案

Android12以下扫描不到设备

缺BLUETOOTH/BLUETOOTH_ADMIN权限

按 SDK 版本动态申请权限

拍照返回图片方向不对

眼镜摄像头旋转 90°

传入Size(1024, 768)而非Size(768, 1024)

takeGlassPhoto vs takeGlassPhotoGlobal

前者返回路径,后者返回字节数组

需要即时处理用 Global,保存到眼镜用路径版

 Wi-Fi P2P 连接失败

眼镜和手机不在同一网络

确保眼镜Wi-Fi已开启,SDK 会自动处理 P2P 协商

自定义视图不显示

 JSON 格式错误或缺少必要字段

检查 type/props/children 结构,确保 id字段存在

.lc`鉴权失败

文件路径或 CLIENT_SECRET不匹配

确认 .lc`文件在 res/raw/下,密钥与 Rokid 开发者平台一致

 如果本文对你有帮助,欢迎点赞收藏,有问题欢迎评论区交流!

赞(0)
未经允许不得转载:171主机测评 » Rokid 智能眼镜开发实战:手机端 + 眼镜端双端协同 App 完整指南
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址