写点什么

Rokid Glasses 移动端控制应用开发初体验 - 助力业务创新

作者:轻口味
  • 2025-10-13
    北京
  • 本文字数:32684 字

    阅读完需:约 107 分钟

Rokid Glasses 移动端控制应用开发初体验-助力业务创新

前言

在 AI 时代,一方面大家在提升模型这个”大脑“的能力,另一方面也在不断地给”大脑“配备各种”外设“,录音笔和 AI 眼镜就是很好的切入点。而 AI 眼镜因为与人眼、人耳处在同一个角度,可以以更自然真实的角度去采集音频与视频,"第一视角拍摄"和"长在眼前的 AI 助手"成为大家采购智能设备的首选。本文介绍 AI 眼镜的佼佼者 Rokid Glasses 的产品、能力,以及如何从零开发一个 Rokid Glasses 配套的手机应用,实现销售与客户沟通过程分析,帮助销售人员提效。

## 项目介绍

在企业销售场景中,销售人员与客户面对面的沟通包含大量隐性信息:


  • 客户的语气变化、提问重点、兴趣方向;

  • 对价格、服务、品牌等要素的关注程度;

  • 情绪波动与潜在购买意向。这些关键信息往往无法被实时记录,事后人工回忆也存在大量偏差。

  • 因此,我们希望利用 Rokid 设备的语音采集能力 和 App 的控制与数据传输能力,构建一套智能销售助手系统,让销售过程“可听、可见、可分析”。

Rokid 开发者工具介绍

Rokid 提供了两种类型设备 Rokid AR 与 Rokid Glasses,分别对应 YodaOS-Master 和 YodaOS-Sprite 系统,本文主要介绍基于 Rokid Glasses 配套 CXR-M SDK 开发手机应用。


CXR-M SDK 是面向移动端的开发工具包,主要用于构建手机端与 Rokid Glasses 的控制和协同应用。开发者可以通过 CXR-M SDK 与眼镜建立稳定连接,实现数据通信、实时音视频获取以及场景自定义。它适合需要在手机端进行界面交互、远程控制或与眼镜端配合完成复杂功能的应用。目前 CXR-M SDK 仅提供 Android 版本,正好作为一名 Androider 快速上手一波。


首先用一张官方的图片来介绍眼镜设备与手机之间的关系:



从图中可以看到,眼镜是一个基于 AOSP 的操作系统,手机通过 Rokid Glasses Protocol 与眼镜设备来交互。


通过 CXR-M SDK 可以与眼镜进行如下信息交互:


  • 获取眼镜设备系统信息

  • 设备连接

  • AI 能力

  • 录音服务

  • 拍照服务

  • 录像服务

  • 飞传功能

  • ...

系统总体架构


  • Rokid 设备:负责语音采集与录音;

  • App 端(核心开发部分):控制录音、下载文件、上传云端;

  • 云端服务:完成语音识别与画像分析。本文主要介绍 App 端功能开发,APP 主要包含以下功能:


  1. 扫描周围 rokid glasses 设备列表

  2. 连接某个设备

  3. 断开设备连接

  4. 停止扫描

  5. 开始录音

  6. 结束录音

  7. 下载 glasses 端录音文件到手机

  8. 删除 glasses 端录音文件

App 端与 Rokid SDK 模块设计与开发

很多眼镜都是配置自己应用,只针对 C 端用户开发,不支持企业和开发者开发应用对接,Rokid 天生支持开发者定制开发,不管是对个人开发者基于硬件创新还是针对企业业务赋能,都提供了很好的支持。接下来基于 Rokid 开发 Rokid Glasses 配套的手机应用。

创建项目

在了解完 CXR-M SDK 能力后,接下来我们基于 CXR-M SDK 开发一个属于我们自己的应用。首先创建项目,在 Android Studio 中新建项目:


接着输入项目名称、包名、存储路径等,Minimum 选择 18,CXR-M SDK 最低支持 Android 9.0,BuildConfigLanguage 选择 Kotlin DSL,CXR-M SDK 选用 Kotlin 语言:


点击 Finish 按钮完成项目创建,接下来在 settings.gradle.kts 中配置阿里云镜像,更快的 sync 工程。


repositories {    maven { url = uri("https://maven.aliyun.com/repository/google") }    maven { url = uri("https://maven.aliyun.com/repository/central") }    maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }    google {      content {        includeGroupByRegex("com\\.android.*")        includeGroupByRegex("com\\.google.*")        includeGroupByRegex("androidx.*")      }    }    mavenCentral()    gradlePluginPortal()  }
复制代码

配置依赖

CXR-M SDK 采用 Maven 在线管理 SDK Package,在 settings.gradle.kts 添加 maven 仓库配置,maven { url = uri("https://maven.rokid.com/repository/maven-public/") }



增加配置后同步工程,同步成功后添加 CXR-M SDK 依赖:implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2") 除了依赖 rokid client-m 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 ("com.squareup.okhttp3:logging-interceptor:4.9.1")
复制代码



配置完成后同步工程,同步成功后就可以使用 CXR-M SDK 进行功能开发了。

扫描连接设备

Rokid Glasses 使用蓝牙与手机通信,手机端做任何动作都需要与眼镜端建立蓝牙连接,首先需要通过 Android 系统标准蓝牙接口扫描发现周边设备。CXR-M SDK 通过 Android 系统 API 扫描发现设备,扫描过程中可以使用 UUID:00009100-0000-1000-8000-00805f9b34fb,来过滤 Rokid 的设备。连接设备需要执行两步操作:


  1. 扫描设备列表

  2. 选择设备连接


在我们页面中增加扫描设备、停止扫描按钮,以列表形式展示扫描到的设备列表,选中某个设备点击连接和进行手机与设备的蓝牙连接。这里简单介绍下 UUID,BLE(Bluetooth Low Energy,蓝牙低功耗)UUID(Universally Unique Identifier,通用唯一标识符)是 BLE 协议栈中核心标识元素,本质是一个 128 位的数字标签,用于唯一标识 BLE 设备中的服务(Service)、特性(Characteristic)和描述符(Descriptor)。它确保了不同设备、同一设备的不同功能模块在全球范围内具有唯一性,是 BLE 设备实现互联互通的基础——就像每个人的身份证号码一样,BLE UUID 让设备能准确识别并交互所需的功能。


为简化开发并促进跨设备兼容,蓝牙特殊兴趣小组(SIG)定义了大量标准服务​(如心率监测、电池电量、设备信息)的 UUID,采用“16 位短 UUID”形式(部分场景需扩展为 128 位)。这些标准 UUID 是全球 BLE 设备的“通用语言”,例如:


  • 心率服务​:16 位 UUID 为0x180D,对应全 128 位 UUID 为0000180D-0000-1000-8000-00805F9B34FB,用于标识设备的心率监测功能;

  • 电池服务​:16 位 UUID 为0x180F,对应全 128 位 UUID 为0000180F-0000-1000-8000-00805F9B34FB,用于标识设备的电池电量信息;

  • 设备信息服务​:16 位 UUID 为0x180A,对应全 128 位 UUID 为0000180A-0000-1000-8000-00805F9B34FB,用于标识设备的基本信息(如制造商、型号)。 标准 UUID 的存在,让不同厂商的 BLE 设备(如智能手环、健康监测仪)能无缝交互——例如,任何支持心率服务的手机都能识别并读取0x180D服务下的心率数据。Rokid Glasses 使用的 UUID 为 00009100-0000-1000-8000-00805f9b34fb。


页面对应的 ViewModel 实现如下:


import android.Manifestimport android.annotation.SuppressLintimport android.app.Applicationimport android.bluetooth.BluetoothAdapterimport android.bluetooth.BluetoothDeviceimport android.bluetooth.BluetoothManagerimport android.bluetooth.le.ScanCallbackimport android.bluetooth.le.ScanFilterimport android.bluetooth.le.ScanResultimport android.bluetooth.le.ScanSettingsimport android.content.Contextimport android.os.ParcelUuidimport android.util.Logimport androidx.annotation.RequiresPermissionimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.LiveDataimport androidx.lifecycle.MutableLiveDataimport androidx.lifecycle.viewModelScopeimport com.qingkouwei.rokidclient.controller.RokidRecorderControllerimport com.qingkouwei.rokidclient.model.Deviceimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport kotlinx.coroutines.Dispatchersimport java.util.concurrent.ConcurrentHashMapimport com.rokid.cxr.client.extend.CxrApiimport com.rokid.cxr.client.extend.callbacks.BluetoothStatusCallbackimport com.rokid.cxr.client.utils.ValueUtil
/** * 设备扫描 ViewModel * 管理设备扫描和连接逻辑 */class DeviceScanViewModel(application: Application) : AndroidViewModel(application) { companion object { private const val TAG = "DeviceScanViewModel" private const val ROKID_SERVICE_UUID = "00009100-0000-1000-8000-00805f9b34fb" // Rokid Glasses Service } private val controller = RokidRecorderController(application) private val context = application.applicationContext // 蓝牙相关 private var bluetoothAdapter: BluetoothAdapter? = null private var bluetoothLeScanner: android.bluetooth.le.BluetoothLeScanner? = null // 扫描结果存储 private val scanResultMap: ConcurrentHashMap<String, BluetoothDevice> = ConcurrentHashMap() private val bondedDeviceMap: ConcurrentHashMap<String, BluetoothDevice> = ConcurrentHashMap() // 扫描状态 private val _isScanning = MutableLiveData<Boolean>() val isScanning: LiveData<Boolean> = _isScanning // 设备列表 private val _devices = MutableLiveData<List<Device>>() val devices: LiveData<List<Device>> = _devices // 连接状态 private val _isConnecting = MutableLiveData<Boolean>() val isConnecting: LiveData<Boolean> = _isConnecting // 连接结果 private val _connectionResult = MutableLiveData<Boolean?>() val connectionResult: LiveData<Boolean?> = _connectionResult // 状态消息 private val _statusMessage = MutableLiveData<String>() val statusMessage: LiveData<String> = _statusMessage // 连接信息 private val _connectionInfo = MutableLiveData<ConnectionInfo?>() val connectionInfo: LiveData<ConnectionInfo?> = _connectionInfo // 当前连接的设备 private val _currentConnectedDevice = MutableLiveData<Device?>() val currentConnectedDevice: LiveData<Device?> = _currentConnectedDevice // 连接信息数据类 data class ConnectionInfo( val socketUuid: String, val macAddress: String, val rokidAccount: String?, val glassesType: Int ) // Rokid CXR SDK 蓝牙状态回调 private val bluetoothStatusCallback = object : BluetoothStatusCallback { override fun onConnectionInfo( socketUuid: String?, macAddress: String?, rokidAccount: String?, glassesType: Int ) { Log.d(TAG, "收到连接信息: socketUuid=$socketUuid, macAddress=$macAddress, rokidAccount=$rokidAccount, glassesType=$glassesType") socketUuid?.let { uuid -> macAddress?.let { address -> val connectionInfo = ConnectionInfo( socketUuid = uuid, macAddress = address, rokidAccount = rokidAccount, glassesType = glassesType ) _connectionInfo.value = connectionInfo // 自动进行连接 connectBluetooth(uuid, address) } ?: run { Log.e(TAG, "macAddress is null") _statusMessage.value = "设备 MAC 地址为空" } } ?: run { Log.e(TAG, "socketUuid is null") _statusMessage.value = "设备 UUID 为空" } }
override fun onConnected() { Log.d(TAG, "蓝牙连接成功") _statusMessage.value = "蓝牙连接成功" _connectionResult.value = true _isConnecting.value = false }
override fun onDisconnected() { Log.d(TAG, "蓝牙连接断开") _statusMessage.value = "蓝牙连接断开" _connectionResult.value = false _isConnecting.value = false _currentConnectedDevice.value = null }
override fun onFailed(errorCode: ValueUtil.CxrBluetoothErrorCode?) { Log.e(TAG, "蓝牙连接失败: $errorCode") val errorMessage = when (errorCode) { ValueUtil.CxrBluetoothErrorCode.PARAM_INVALID -> "参数无效" ValueUtil.CxrBluetoothErrorCode.BLE_CONNECT_FAILED -> "BLE 连接失败" ValueUtil.CxrBluetoothErrorCode.SOCKET_CONNECT_FAILED -> "Socket 连接失败" ValueUtil.CxrBluetoothErrorCode.UNKNOWN -> "未知错误" else -> "连接失败" } _statusMessage.value = "蓝牙连接失败: $errorMessage" _connectionResult.value = false _isConnecting.value = false } } // 蓝牙扫描回调 private val scanCallback = object : ScanCallback() { @SuppressLint("MissingPermission") override fun onScanResult(callbackType: Int, result: ScanResult?) { super.onScanResult(callbackType, result) result?.let { scanResult -> scanResult.device.name?.let { deviceName -> // 检查是否是 Rokid 设备 if (deviceName.contains("Rokid", ignoreCase = true) || deviceName.contains("Glasses", ignoreCase = true)) { scanResultMap[deviceName] = scanResult.device val rssi = scanResult.rssi updateDeviceList() Log.d(TAG, "发现 Rokid 设备: $deviceName") } } } } override fun onScanFailed(errorCode: Int) { super.onScanFailed(errorCode) Log.e(TAG, "扫描失败,错误代码: $errorCode") _statusMessage.value = "扫描失败,错误代码: $errorCode" _isScanning.value = false } } init { _devices.value = emptyList() _isScanning.value = false _isConnecting.value = false _statusMessage.value = "点击开始扫描设备" // 初始化蓝牙适配器 initializeBluetooth() } /** * 初始化蓝牙适配器 */ private fun initializeBluetooth() { try { val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager bluetoothAdapter = bluetoothManager.adapter if (bluetoothAdapter == null) { Log.e(TAG, "设备不支持蓝牙") _statusMessage.value = "设备不支持蓝牙" return } if (!bluetoothAdapter!!.isEnabled) { Log.w(TAG, "蓝牙未开启") _statusMessage.value = "请先开启蓝牙" return } bluetoothLeScanner = bluetoothAdapter!!.bluetoothLeScanner Log.d(TAG, "蓝牙初始化成功") _statusMessage.value = "蓝牙已就绪,可以开始扫描" } catch (e: Exception) { Log.e(TAG, "蓝牙初始化失败", e) _statusMessage.value = "蓝牙初始化失败: ${e.message}" } } /** * 开始扫描设备 */ fun startScanning() { if (_isScanning.value == true) return if (bluetoothLeScanner == null) { _statusMessage.value = "蓝牙未初始化,请检查蓝牙状态" return } _isScanning.value = true _statusMessage.value = "正在扫描附近的Rokid设备..." viewModelScope.launch { try { // 清空之前的扫描结果 scanResultMap.clear() // 先检查已配对的设备 checkBondedDevices() // 开始蓝牙 LE 扫描 startBluetoothLeScan() } catch (e: Exception) { Log.e(TAG, "开始扫描失败", e) _statusMessage.value = "扫描失败: ${e.message}" _isScanning.value = false } } } /** * 检查已配对的设备 */ @SuppressLint("MissingPermission") private fun checkBondedDevices() { bluetoothAdapter?.bondedDevices?.forEach { device -> device.name?.let { deviceName -> if (deviceName.contains("Rokid", ignoreCase = true) || deviceName.contains("Glasses", ignoreCase = true)) { bondedDeviceMap[deviceName] = device Log.d(TAG, "发现已配对的 Rokid 设备: $deviceName") } } } updateDeviceList() } /** * 开始蓝牙 LE 扫描 */ @SuppressLint("MissingPermission") @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) private fun startBluetoothLeScan() { try { val scanFilter = ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(ROKID_SERVICE_UUID)) .build() val scanSettings = ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build() bluetoothLeScanner?.startScan( listOf(scanFilter), scanSettings, scanCallback ) Log.d(TAG, "开始蓝牙 LE 扫描") } catch (e: Exception) { Log.e(TAG, "启动蓝牙 LE 扫描失败", e) _statusMessage.value = "启动扫描失败: ${e.message}" _isScanning.value = false } } /** * 停止扫描 */ @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) fun stopScanning() { _isScanning.value = false _statusMessage.value = "扫描已停止" try { bluetoothLeScanner?.stopScan(scanCallback) Log.d(TAG, "停止蓝牙 LE 扫描") } catch (e: Exception) { Log.e(TAG, "停止扫描失败", e) _statusMessage.value = "停止扫描失败: ${e.message}" } } /** * 更新设备列表 */ private fun updateDeviceList() { val deviceList = mutableListOf<Device>() // 添加扫描到的设备 scanResultMap.forEach { (deviceName, bluetoothDevice) -> val device = Device( deviceId = bluetoothDevice.address, deviceName = deviceName, ipAddress = bluetoothDevice.address, isConnected = false, batteryLevel = 0, // 蓝牙设备无法直接获取电量 signalStrength = 0 // 蓝牙设备无法直接获取信号强度 ) deviceList.add(device) } // 添加已配对的设备 bondedDeviceMap.forEach { (deviceName, bluetoothDevice) -> val device = Device( deviceId = bluetoothDevice.address, deviceName = deviceName, ipAddress = bluetoothDevice.address, isConnected = false, batteryLevel = 0, signalStrength = 0 ) // 避免重复添加 if (deviceList.none { it.deviceId == device.deviceId }) { deviceList.add(device) } } _devices.value = deviceList if (deviceList.isEmpty()) { _statusMessage.value = "未发现设备,请确保设备已开启" } else { _statusMessage.value = "发现 ${deviceList.size} 个设备" } } /** * 连接到指定设备 */ fun connectToDevice(device: Device) { if (_isConnecting.value == true) return _isConnecting.value = true _statusMessage.value = "正在连接到 ${device.deviceName}..." viewModelScope.launch { try { // 使用蓝牙连接设备 val success = connectBluetoothDevice(device) _connectionResult.value = success if (success) { _statusMessage.value = "连接成功" } else { _statusMessage.value = "连接失败,请重试" } } catch (e: Exception) { _statusMessage.value = "连接失败: ${e.message}" _connectionResult.value = false } finally { _isConnecting.value = false } } } /** * 使用蓝牙连接设备 - 使用 Rokid CXR SDK */ @SuppressLint("MissingPermission") @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private suspend fun connectBluetoothDevice(device: Device): Boolean = withContext(Dispatchers.IO) { try { // 查找对应的蓝牙设备 val bluetoothDevice = scanResultMap.values.find { it.address == device.deviceId } ?: bondedDeviceMap.values.find { it.address == device.deviceId } if (bluetoothDevice == null) { Log.e(TAG, "未找到对应的蓝牙设备: ${device.deviceName}") return@withContext false } Log.d(TAG, "开始使用 Rokid CXR SDK 初始化蓝牙设备: ${device.deviceName}") // 使用 Rokid CXR SDK 初始化蓝牙 initBluetooth(bluetoothDevice) // 设置当前连接的设备 _currentConnectedDevice.value = device.copy(isConnected = true) true } catch (e: Exception) { Log.e(TAG, "连接蓝牙设备失败", e) false } } /** * 初始化蓝牙 - 使用 Rokid CXR SDK */ private fun initBluetooth(device: BluetoothDevice) { try { Log.d(TAG, "调用 CxrApi.initBluetooth") CxrApi.getInstance().initBluetooth(context, device, bluetoothStatusCallback) } catch (e: Exception) { Log.e(TAG, "初始化蓝牙失败", e) _statusMessage.value = "初始化蓝牙失败: ${e.message}" _connectionResult.value = false _isConnecting.value = false } } /** * 连接蓝牙 - 使用 Rokid CXR SDK */ private fun connectBluetooth(socketUuid: String, macAddress: String) { try { Log.d(TAG, "调用 CxrApi.connectBluetooth: socketUuid=$socketUuid, macAddress=$macAddress") CxrApi.getInstance().connectBluetooth(context, socketUuid, macAddress, bluetoothStatusCallback) } catch (e: Exception) { Log.e(TAG, "连接蓝牙失败", e) _statusMessage.value = "连接蓝牙失败: ${e.message}" _connectionResult.value = false _isConnecting.value = false } } /** * 释放资源,Rokid CXR SDK 反初始化 */ @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) fun release() { try { // 停止扫描 if (_isScanning.value == true) { stopScanning() } // 反初始化 Rokid CXR SDK 蓝牙 try { CxrApi.getInstance().deinitBluetooth() Log.d(TAG, "Rokid CXR SDK 蓝牙已反初始化") } catch (e: Exception) { Log.e(TAG, "反初始化 Rokid CXR SDK 蓝牙失败", e) } // 清空扫描结果 scanResultMap.clear() bondedDeviceMap.clear() // 重置状态 _devices.value = emptyList() _isScanning.value = false _isConnecting.value = false _connectionInfo.value = null _currentConnectedDevice.value = null _statusMessage.value = "资源已释放" Log.d(TAG, "DeviceScanViewModel 资源已释放") } catch (e: Exception) { Log.e(TAG, "释放资源失败", e) } } /** * 获取蓝牙连接状态 */ fun isBluetoothConnected(): Boolean { return try { CxrApi.getInstance().isBluetoothConnected } catch (e: Exception) { Log.e(TAG, "获取蓝牙连接状态失败", e) false } } /** * 断开蓝牙连接 */ fun disconnectBluetooth() { try { CxrApi.getInstance().deinitBluetooth() _currentConnectedDevice.value = null _connectionInfo.value = null _statusMessage.value = "蓝牙连接已断开" Log.d(TAG, "蓝牙连接已断开") } catch (e: Exception) { Log.e(TAG, "断开蓝牙连接失败", e) } } }
复制代码


点击页面中开始扫描,调用上面代码中 startScan 方法开始扫描,方法中首选获取已连接设备列表, BluetoothAdapter 的 bondedDevices 标识所有已配对设备信息,通过过滤返回的已配对设备的 isConnected 字段来判断已配对设备中哪些是已连接设备。接着根据设备名称中是否包含 Glasses 来筛出眼镜设备,更新到应用缓存。


接下来调用 BluetoothLeScanner 的 startScan 方法扫描周围所有蓝牙设备,并晒出 UUID 为 00009100-0000-1000-8000-00805f9b34fb 的所有设备,上面提到过这是 Rokid Glasses Service 的标识。


获取到扫描结果后可以从 ScanResult 中获取设备的信号强度以及扫描到设备的设备名称等信息。


点击扫描到某个设备的连接按钮后调用 connectToDevice 方法连接设备,最终调用 rokid sdk 中的 CxrApi.getInstance().connectBluetooth 方法与设备进行连接,此时我们的手机应用与某一台 Glasses 设备就建立了连接,可以进行其他操作了。


**避坑指南:**在开发手机应用与设备连接是基于蓝牙的,需要申请对应权限:


<!-- 定位权限 -->  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />  <!-- 允许应用获取精确的位置信息,精度较高,通常依赖GPS -->  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />  <!-- 蓝牙权限 -->  <uses-permission android:name="android.permission.BLUETOOTH" />  <!-- 允许应用管理蓝牙连接,如发现和配对新设备 -->  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />  <!-- Android 12及以上新增,允许应用主动连接到蓝牙设备 -->  <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />  <!-- 蓝牙扫描权限 -->  <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />  <!-- 允许应用改变网络连接状态(如启用/禁用数据连接) -->  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />  <!-- 允许应用获取WiFi网络状态(如WiFi是否开启、连接的网络名称) -->  <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />  <!-- 允许应用改变WiFi状态(如开启/关闭WiFi、连接指定WiFi) -->  <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" />
复制代码


其中 ACCESS_FINE_LOCATION、Manifest.permission.BLUETOOTH、Manifest.permission.BLUETOOTH_ADMIN 是敏感权限,需要动态申请。在使用 SDK 之前弹出授权对话框进行权限申请。

启动录音

手机应用与设备建立连接后,接下来我们通过调用 SDK 接口来启动 Glasses 设备开始录音。可以通过 CXR-M SDK 的fun openAudioRecord(codecType: Int, streamType: String?): ValueUtil.CxrStatus?接口,开启录音,通过fun closeAudioRecord(streamType: String): ValueUtil.CxrStatus?关闭录音,并通过fun setVideoStreamListener(callback: AudioStreamListener)设置回调监听录音结果。


在控制页面增加开始录音、结束录音、断开连接、文件管理等按钮,UI 效果图如下:


页面中点击搜索设备跳转“扫描连接设备”小节中的扫描页面,连接成功后开始录音置为可以点击,点击后开始录音。控制页面对应 ViewModel 实现代码如下:


package com.qingkouwei.rokidclient.viewmodel
import android.app.Applicationimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.LiveDataimport androidx.lifecycle.MutableLiveDataimport androidx.lifecycle.viewModelScopeimport com.qingkouwei.rokidclient.controller.RokidRecorderControllerimport com.qingkouwei.rokidclient.model.AnalysisResultimport com.qingkouwei.rokidclient.model.Deviceimport com.qingkouwei.rokidclient.model.Recordingimport com.qingkouwei.rokidclient.network.MockAnalysisServiceimport com.rokid.cxr.client.extend.CxrApiimport com.rokid.cxr.client.extend.callbacks.BluetoothStatusCallbackimport com.rokid.cxr.client.utils.ValueUtilimport com.rokid.cxr.client.extend.listeners.AudioStreamListenerimport kotlinx.coroutines.flow.collectimport kotlinx.coroutines.launchimport okhttp3.MediaType.Companion.toMediaTypeOrNullimport okhttp3.MultipartBodyimport okhttp3.RequestBody.Companion.asRequestBodyimport java.io.Fileimport java.io.FileOutputStreamimport java.io.IOExceptionimport java.text.SimpleDateFormatimport java.util.*
/** * 主界面 ViewModel * 管理设备连接、录音控制和文件上传分析 */class MainViewModel(application: Application) : AndroidViewModel(application) { private val recorderController = RokidRecorderController(application) private val analysisService = MockAnalysisService() // Rokid CXR SDK 实例 private val cxrApi = CxrApi.getInstance() // 连接信息 private val _connectionInfo = MutableLiveData<ConnectionInfo?>() val connectionInfo: LiveData<ConnectionInfo?> = _connectionInfo // 连接信息数据类 data class ConnectionInfo( val socketUuid: String, val macAddress: String, val rokidAccount: String?, val glassesType: Int ) // 设备相关状态 private val _devices = MutableLiveData<List<Device>>() val devices: LiveData<List<Device>> = _devices private val _isConnected = MutableLiveData<Boolean>() val isConnected: LiveData<Boolean> = _isConnected private val _currentDevice = MutableLiveData<Device?>() val currentDevice: LiveData<Device?> = _currentDevice // 录音相关状态 private val _isRecording = MutableLiveData<Boolean>() val isRecording: LiveData<Boolean> = _isRecording private val _recordingDuration = MutableLiveData<Long>() val recordingDuration: LiveData<Long> = _recordingDuration private val _downloadProgress = MutableLiveData<Int>() val downloadProgress: LiveData<Int> = _downloadProgress // 录音流类型 private val _currentStreamType = MutableLiveData<String?>() val currentStreamType: LiveData<String?> = _currentStreamType // 录音数据 private val _audioData = MutableLiveData<ByteArray?>() val audioData: LiveData<ByteArray?> = _audioData // 录音文件相关 private var currentRecordingFile: File? = null private var fileOutputStream: FileOutputStream? = null private val recordingDirectory = File(application.getExternalFilesDir(null), "recordings") // 录音文件路径 private val _currentRecordingPath = MutableLiveData<String?>() val currentRecordingPath: LiveData<String?> = _currentRecordingPath // 分析结果 private val _analysisResult = MutableLiveData<AnalysisResult?>() val analysisResult: LiveData<AnalysisResult?> = _analysisResult // 状态消息 private val _statusMessage = MutableLiveData<String>() val statusMessage: LiveData<String> = _statusMessage // 加载状态 private val _isLoading = MutableLiveData<Boolean>() val isLoading: LiveData<Boolean> = _isLoading // 音频流监听器 private val audioStreamListener = object : AudioStreamListener { override fun onStartAudioStream(codecType: Int, streamType: String?) { _currentStreamType.value = streamType _statusMessage.value = "录音已开始: $streamType" // 创建录音文件 createRecordingFile(streamType) }
override fun onAudioStream(data: ByteArray?, offset: Int, length: Int) { data?.let { audioData -> val validData = audioData.sliceArray(offset until offset + length) _audioData.value = validData // 实时写入文件 writeAudioDataToFile(validData) } } } init { observeControllerStates() initializeRecordingDirectory() } /** * 观察控制器状态变化 */ private fun observeControllerStates() { viewModelScope.launch { recorderController.isConnected.collect { connected -> _isConnected.value = connected } } viewModelScope.launch { recorderController.isRecording.collect { recording -> _isRecording.value = recording } } viewModelScope.launch { recorderController.currentDevice.collect { device -> _currentDevice.value = device } } viewModelScope.launch { recorderController.recordingDuration.collect { duration -> _recordingDuration.value = duration } } viewModelScope.launch { recorderController.downloadProgress.collect { progress -> _downloadProgress.value = progress } } } /** * 连接到指定设备 */ fun connectToDevice(device: Device) { viewModelScope.launch { try { _isLoading.value = true _statusMessage.value = "正在连接到 ${device.deviceName}..." val success = recorderController.connectToDevice(device) if (success) { _statusMessage.value = "设备连接成功" } else { _statusMessage.value = "设备连接失败" } } catch (e: Exception) { _statusMessage.value = "连接异常: ${e.message}" } finally { _isLoading.value = false } } } /** * 开始录音 */ fun startRecording(codecType: Int = 1, streamType: String = "AI_assistant") { viewModelScope.launch { try { _statusMessage.value = "开始录音..." // 检查蓝牙连接状态 if (!cxrApi.isBluetoothConnected) { _statusMessage.value = "蓝牙未连接,无法开始录音" return@launch } // 设置音频流监听器 cxrApi.setAudioStreamListener(audioStreamListener) // 开启录音 val status = cxrApi.openAudioRecord(codecType, streamType) when (status) { ValueUtil.CxrStatus.REQUEST_SUCCEED -> { _isRecording.value = true _currentStreamType.value = streamType _statusMessage.value = "录音已开始" } ValueUtil.CxrStatus.REQUEST_WAITING -> { _statusMessage.value = "录音请求等待中" } ValueUtil.CxrStatus.REQUEST_FAILED -> { _statusMessage.value = "录音开始失败" } else -> { _statusMessage.value = "录音状态未知: $status" } } } catch (e: Exception) { _statusMessage.value = "录音异常: ${e.message}" } } } /** * 停止录音 */ fun stopRecording(streamType: String = "AI_assistant") { viewModelScope.launch { try { _statusMessage.value = "停止录音..." // 检查录音状态 if (_isRecording.value != true) { _statusMessage.value = "当前没有进行录音" return@launch } // 关闭录音 val status = cxrApi.closeAudioRecord(streamType) when (status) { ValueUtil.CxrStatus.REQUEST_SUCCEED -> { _isRecording.value = false _currentStreamType.value = null _statusMessage.value = "录音已停止" // 关闭录音文件 closeRecordingFile() } ValueUtil.CxrStatus.REQUEST_WAITING -> { _statusMessage.value = "录音停止请求等待中" } ValueUtil.CxrStatus.REQUEST_FAILED -> { _statusMessage.value = "录音停止失败" } else -> { _statusMessage.value = "录音停止状态未知: $status" } } // 移除音频流监听器 cxrApi.setAudioStreamListener(null) } catch (e: Exception) { _statusMessage.value = "停止录音异常: ${e.message}" } } } /** * 停止录音并下载文件 */ fun stopRecordingAndDownload() { viewModelScope.launch { try { _isLoading.value = true _statusMessage.value = "停止录音并下载文件..." val recording = recorderController.stopRecordingAndDownload() if (recording != null) { _statusMessage.value = "录音文件下载完成" // 自动上传并分析 uploadAndAnalyzeRecording(recording) } else { _statusMessage.value = "录音停止失败" } } catch (e: Exception) { _statusMessage.value = "停止录音异常: ${e.message}" } finally { _isLoading.value = false } } } /** * 上传录音文件并进行分析 */ private fun uploadAndAnalyzeRecording(recording: Recording) { viewModelScope.launch { try { _statusMessage.value = "正在上传文件并分析..." val file = File(recording.localPath) if (!file.exists()) { _statusMessage.value = "录音文件不存在" return@launch } // 创建 MultipartBody.Part val requestFile = file.asRequestBody("audio/wav".toMediaTypeOrNull()) val audioPart = MultipartBody.Part.createFormData("audio", file.name, requestFile) // 调用分析服务 val response = analysisService.uploadAudio(audioPart) if (response.isSuccessful) { val result = response.body() if (result != null) { _analysisResult.value = result _statusMessage.value = "分析完成" } else { _statusMessage.value = "分析结果为空" } } else { _statusMessage.value = "上传失败: ${response.code()}" } } catch (e: Exception) { _statusMessage.value = "上传分析异常: ${e.message}" } } } /** * 断开设备连接 */ fun disconnectDevice() { viewModelScope.launch { try { // 先停止录音 if (_isRecording.value == true) { stopRecording() } // 断开蓝牙连接 cxrApi.deinitBluetooth() // 重置状态 _isConnected.value = false _currentDevice.value = null _connectionInfo.value = null _currentRecordingPath.value = null _statusMessage.value = "设备已断开连接" } catch (e: Exception) { _statusMessage.value = "断开连接异常: ${e.message}" } } } /** * 格式化录音时长显示 */ fun formatDuration(durationMs: Long): String { val seconds = durationMs / 1000 val minutes = seconds / 60 val remainingSeconds = seconds % 60 return String.format("%02d:%02d", minutes, remainingSeconds) } override fun onCleared() { super.onCleared() recorderController.cleanup() // 关闭录音文件 closeRecordingFile() // 释放 Rokid CXR SDK 资源 try { cxrApi.deinitBluetooth() } catch (e: Exception) { // 忽略异常 } } /** * 初始化录音目录 */ private fun initializeRecordingDirectory() { try { if (!recordingDirectory.exists()) { recordingDirectory.mkdirs() } } catch (e: Exception) { android.util.Log.e("MainViewModel", "创建录音目录失败", e) } } /** * 创建录音文件 */ private fun createRecordingFile(streamType: String?) { try { val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) val fileName = "recording_${streamType ?: "unknown"}_$timestamp.pcm" currentRecordingFile = File(recordingDirectory, fileName) fileOutputStream = FileOutputStream(currentRecordingFile) _currentRecordingPath.value = currentRecordingFile?.absolutePath android.util.Log.d("MainViewModel", "创建录音文件: ${currentRecordingFile?.absolutePath}") } catch (e: Exception) { android.util.Log.e("MainViewModel", "创建录音文件失败", e) _statusMessage.value = "创建录音文件失败: ${e.message}" } } /** * 实时写入音频数据到文件 */ private fun writeAudioDataToFile(audioData: ByteArray) { try { fileOutputStream?.write(audioData) fileOutputStream?.flush() } catch (e: IOException) { android.util.Log.e("MainViewModel", "写入音频数据失败", e) _statusMessage.value = "写入音频数据失败: ${e.message}" } } /** * 关闭录音文件 */ private fun closeRecordingFile() { try { fileOutputStream?.close() fileOutputStream = null val filePath = currentRecordingFile?.absolutePath android.util.Log.d("MainViewModel", "录音文件已保存: $filePath") if (filePath != null) { _statusMessage.value = "录音文件已保存: ${File(filePath).name}" } } catch (e: Exception) { android.util.Log.e("MainViewModel", "关闭录音文件失败", e) _statusMessage.value = "关闭录音文件失败: ${e.message}" } finally { currentRecordingFile = null } } /** * 获取录音文件大小 */ fun getRecordingFileSize(): Long { return currentRecordingFile?.length() ?: 0L } /** * 格式化文件大小显示 */ fun formatFileSize(bytes: Long): String { return when { bytes < 1024 -> "$bytes B" bytes < 1024 * 1024 -> "${bytes / 1024} KB" bytes < 1024 * 1024 * 1024 -> "${bytes / (1024 * 1024)} MB" else -> "${bytes / (1024 * 1024 * 1024)} GB" } }}
复制代码


  • 点击开始录音按钮调用 stopRecording 方法,Rokid Glasses 录制的音频格式支持 opus 和 pcm,都支持流式存储,所以通过 audioStreamListener 可以在录音过程中实时的将音频信息下载到手机,边录音边传输到手机。

  • 点击结束录音按钮调用 stopRecording 方法,停止设备录音。

  • 点击断开连接调用 disconnectDevice 断开设备连接。

同步文件

上面提到可以边录边将音频传输到手机,但是如果录音过程中手机与设备断连,此时没办法实时下载音频,可以在录音结设备连接后主动从设备拉取音频文件。另一方面录音时一直连接设备会增加设备耗电,可以从硬件直接开启录音,录音结束后统一下载文件。CXR-M SDK 提供了获取设备文件和同步文件的方法。点击操作页管理设备文件可以查看设备端文件。


设备文件页面对应 ViewModel 代码实现如下:


package com.qingkouwei.rokidclient.viewmodel
import android.app.Applicationimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.LiveDataimport androidx.lifecycle.MutableLiveDataimport androidx.lifecycle.viewModelScopeimport com.qingkouwei.rokidclient.controller.RokidRecorderControllerimport com.qingkouwei.rokidclient.model.DeviceFileimport com.qingkouwei.rokidclient.model.FileTypeimport kotlinx.coroutines.delayimport kotlinx.coroutines.launchimport java.io.Fileimport java.text.SimpleDateFormatimport java.util.*import android.util.Log// 导入 Rokid CXR SDKimport com.rokid.cxr.client.extend.CxrApiimport com.rokid.cxr.client.extend.callbacks.UnsyncNumResultCallbackimport com.rokid.cxr.client.extend.listeners.MediaFilesUpdateListenerimport com.rokid.cxr.client.extend.callbacks.SyncStatusCallbackimport com.rokid.cxr.client.extend.callbacks.WifiP2PStatusCallbackimport com.rokid.cxr.client.utils.ValueUtil
/** * 设备文件管理 ViewModel * 管理设备文件列表、下载、上传和删除操作 */class DeviceFilesViewModel(application: Application) : AndroidViewModel(application) { companion object { private const val TAG = "DeviceFilesViewModel" } private val controller = RokidRecorderController(application) private val context = application.applicationContext // Rokid CXR SDK 实例 private val cxrApi = CxrApi.getInstance() // 本地存储目录 private val localStorageDir = File(context.getExternalFilesDir(null), "device_files") // 设备文件列表 private val _deviceFiles = MutableLiveData<List<DeviceFile>>() val deviceFiles: LiveData<List<DeviceFile>> = _deviceFiles // 设备信息 private val _deviceName = MutableLiveData<String>() val deviceName: LiveData<String> = _deviceName private val _fileCount = MutableLiveData<Int>() val fileCount: LiveData<Int> = _fileCount private val _storageInfo = MutableLiveData<String>() val storageInfo: LiveData<String> = _storageInfo // 未同步文件数量 private val _unsyncAudioCount = MutableLiveData<Int>() val unsyncAudioCount: LiveData<Int> = _unsyncAudioCount private val _unsyncPictureCount = MutableLiveData<Int>() val unsyncPictureCount: LiveData<Int> = _unsyncPictureCount private val _unsyncVideoCount = MutableLiveData<Int>() val unsyncVideoCount: LiveData<Int> = _unsyncVideoCount // 操作状态 private val _isLoading = MutableLiveData<Boolean>() val isLoading: LiveData<Boolean> = _isLoading private val _isDownloading = MutableLiveData<Boolean>() val isDownloading: LiveData<Boolean> = _isDownloading private val _isUploading = MutableLiveData<Boolean>() val isUploading: LiveData<Boolean> = _isUploading // 同步状态 private val _isSyncing = MutableLiveData<Boolean>() val isSyncing: LiveData<Boolean> = _isSyncing private val _syncProgress = MutableLiveData<String>() val syncProgress: LiveData<String> = _syncProgress // Wi-Fi 连接状态 private val _isWifiConnected = MutableLiveData<Boolean>() val isWifiConnected: LiveData<Boolean> = _isWifiConnected private val _isWifiConnecting = MutableLiveData<Boolean>() val isWifiConnecting: LiveData<Boolean> = _isWifiConnecting // 状态消息 private val _statusMessage = MutableLiveData<String>() val statusMessage: LiveData<String> = _statusMessage // 下载进度 private val _downloadProgress = MutableLiveData<Map<String, Int>>() val downloadProgress: LiveData<Map<String, Int>> = _downloadProgress // 上传进度 private val _uploadProgress = MutableLiveData<Map<String, Int>>() val uploadProgress: LiveData<Map<String, Int>> = _uploadProgress init { _deviceName.value = "Rokid Air Pro" _fileCount.value = 0 _storageInfo.value = "存储: 2.1GB/8GB" _deviceFiles.value = emptyList() _statusMessage.value = "点击刷新获取设备文件" // 初始化未同步文件数量 _unsyncAudioCount.value = 0 _unsyncPictureCount.value = 0 _unsyncVideoCount.value = 0 // 初始化同步状态 _isSyncing.value = false _syncProgress.value = "" // 初始化 Wi-Fi 状态 _isWifiConnected.value = false _isWifiConnecting.value = false // 初始化本地存储目录 initializeLocalStorage() // 设置媒体文件更新监听器 setupMediaFilesUpdateListener() } /** * 刷新设备文件列表 */ fun refreshDeviceFiles() { viewModelScope.launch { try { _isLoading.value = true _statusMessage.value = "正在获取设备文件..." // 检查蓝牙连接状态 if (!cxrApi.isBluetoothConnected) { _statusMessage.value = "设备未连接,请先连接设备" return@launch } // 获取未同步文件数量 getUnsyncFileCount() // 设置媒体文件更新监听器 setupMediaFilesUpdateListener() _statusMessage.value = "设备文件列表已更新" } catch (e: Exception) { Log.e(TAG, "获取设备文件失败", e) _statusMessage.value = "获取文件失败: ${e.message}" } finally { _isLoading.value = false } } } /** * 下载文件到本地 */ fun downloadFile(deviceFile: DeviceFile) { viewModelScope.launch { try { _isDownloading.value = true _statusMessage.value = "正在下载 ${deviceFile.fileName}..." // 检查蓝牙连接状态 if (!cxrApi.isBluetoothConnected) { _statusMessage.value = "设备未连接,无法下载文件" return@launch } // 检查 Wi-Fi 连接状态,如果未连接则先初始化 Wi-Fi if (!cxrApi.isWifiP2PConnected) { _statusMessage.value = "正在初始化 Wi-Fi 连接..." val wifiInitSuccess = initWifiConnection() if (!wifiInitSuccess) { _statusMessage.value = "Wi-Fi 连接失败,无法下载文件" return@launch } } // 使用 Rokid SDK 同步单个文件 val mediaType = when (deviceFile.fileType) { FileType.AUDIO -> ValueUtil.CxrMediaType.AUDIO FileType.VIDEO -> ValueUtil.CxrMediaType.VIDEO else -> ValueUtil.CxrMediaType.AUDIO } val success = syncSingleFile(deviceFile.fileName, mediaType) if (success) { _statusMessage.value = "下载成功: ${deviceFile.fileName}" // 刷新文件列表 refreshDeviceFiles() } else { _statusMessage.value = "下载失败: ${deviceFile.fileName}" } } catch (e: Exception) { Log.e(TAG, "下载文件失败", e) _statusMessage.value = "下载失败: ${e.message}" } finally { _isDownloading.value = false } } } /** * 上传文件到云端 */ fun uploadFile(deviceFile: DeviceFile) { viewModelScope.launch { try { _isUploading.value = true _statusMessage.value = "正在上传 ${deviceFile.fileName}..." // 检查蓝牙连接状态 if (!cxrApi.isBluetoothConnected) { _statusMessage.value = "设备未连接,无法上传文件" return@launch } // 这里可以实现上传到云端的逻辑 // 暂时模拟上传成功 delay(2000) // 模拟上传时间 _statusMessage.value = "上传成功: ${deviceFile.fileName}" } catch (e: Exception) { Log.e(TAG, "上传文件失败", e) _statusMessage.value = "上传失败: ${e.message}" } finally { _isUploading.value = false } } } /** * 批量下载所有文件 */ fun downloadAllFiles() { viewModelScope.launch { try { _isSyncing.value = true _statusMessage.value = "正在同步所有文件..." // 检查蓝牙连接状态 if (!cxrApi.isBluetoothConnected) { _statusMessage.value = "设备未连接,无法同步文件" return@launch } // 检查 Wi-Fi 连接状态,如果未连接则先初始化 Wi-Fi if (!cxrApi.isWifiP2PConnected) { _statusMessage.value = "正在初始化 Wi-Fi 连接..." val wifiInitSuccess = initWifiConnection() if (!wifiInitSuccess) { _statusMessage.value = "Wi-Fi 连接失败,无法同步文件" return@launch } } // 同步所有类型的文件 val types = arrayOf( ValueUtil.CxrMediaType.AUDIO, ValueUtil.CxrMediaType.PICTURE, ValueUtil.CxrMediaType.VIDEO ) val success = startSyncAllFiles(types) if (success) { _statusMessage.value = "开始同步所有文件" } else { _statusMessage.value = "同步失败" _isSyncing.value = false } } catch (e: Exception) { Log.e(TAG, "同步所有文件失败", e) _statusMessage.value = "同步失败: ${e.message}" _isSyncing.value = false } } } /** * 停止同步 */ fun stopSync() { try { cxrApi.stopSync() _isSyncing.value = false _statusMessage.value = "同步已停止" Log.d(TAG, "同步已停止") } catch (e: Exception) { Log.e(TAG, "停止同步失败", e) _statusMessage.value = "停止同步失败: ${e.message}" } } /** * 批量上传所有文件 */ fun uploadAllFiles() { val files = _deviceFiles.value ?: return files.forEach { file -> uploadFile(file) } } /** * 初始化本地存储目录 */ private fun initializeLocalStorage() { try { if (!localStorageDir.exists()) { localStorageDir.mkdirs() } Log.d(TAG, "本地存储目录已初始化: ${localStorageDir.absolutePath}") } catch (e: Exception) { Log.e(TAG, "初始化本地存储目录失败", e) } } /** * 设置媒体文件更新监听器 */ private fun setupMediaFilesUpdateListener() { try { val mediaFileUpdateListener = object : MediaFilesUpdateListener { override fun onMediaFilesUpdated() { Log.d(TAG, "媒体文件已更新") // 刷新文件列表 refreshDeviceFiles() } } cxrApi.setMediaFilesUpdateListener(mediaFileUpdateListener) Log.d(TAG, "媒体文件更新监听器已设置") } catch (e: Exception) { Log.e(TAG, "设置媒体文件更新监听器失败", e) } } /** * 获取未同步文件数量 */ private fun getUnsyncFileCount() { try { val unSyncCallback = object : UnsyncNumResultCallback { override fun onUnsyncNumResult( status: ValueUtil.CxrStatus?, audioNum: Int, pictureNum: Int, videoNum: Int ) { when (status) { ValueUtil.CxrStatus.RESPONSE_SUCCEED -> { _unsyncAudioCount.value = audioNum _unsyncPictureCount.value = pictureNum _unsyncVideoCount.value = videoNum val totalFiles = audioNum + pictureNum + videoNum _fileCount.value = totalFiles Log.d(TAG, "未同步文件数量 - 音频: $audioNum, 图片: $pictureNum, 视频: $videoNum") _statusMessage.value = "发现 $totalFiles 个未同步文件" } ValueUtil.CxrStatus.RESPONSE_INVALID -> { Log.e(TAG, "获取未同步文件数量失败: 响应无效") _statusMessage.value = "获取文件数量失败: 响应无效" } ValueUtil.CxrStatus.RESPONSE_TIMEOUT -> { Log.e(TAG, "获取未同步文件数量失败: 响应超时") _statusMessage.value = "获取文件数量失败: 响应超时" } else -> { Log.e(TAG, "获取未同步文件数量失败: 未知状态 $status") _statusMessage.value = "获取文件数量失败: 未知状态" } } } } val status = cxrApi.getUnsyncNum(unSyncCallback) when (status) { ValueUtil.CxrStatus.REQUEST_SUCCEED -> { Log.d(TAG, "获取未同步文件数量请求成功") } ValueUtil.CxrStatus.REQUEST_WAITING -> { Log.d(TAG, "获取未同步文件数量请求等待中") _statusMessage.value = "正在获取文件数量..." } ValueUtil.CxrStatus.REQUEST_FAILED -> { Log.e(TAG, "获取未同步文件数量请求失败") _statusMessage.value = "获取文件数量请求失败" } else -> { Log.e(TAG, "获取未同步文件数量请求状态未知: $status") _statusMessage.value = "获取文件数量请求状态未知" } } } catch (e: Exception) { Log.e(TAG, "获取未同步文件数量异常", e) _statusMessage.value = "获取文件数量异常: ${e.message}" } } /** * 同步单个文件 */ private fun syncSingleFile(fileName: String, mediaType: ValueUtil.CxrMediaType): Boolean { return try { val syncCallback = object : SyncStatusCallback { override fun onSyncStart() { Log.d(TAG, "开始同步文件: $fileName") _syncProgress.value = "开始同步: $fileName" } override fun onSingleFileSynced(fileName: String?) { Log.d(TAG, "文件同步成功: $fileName") _syncProgress.value = "同步成功: $fileName" } override fun onSyncFailed() { Log.e(TAG, "文件同步失败: $fileName") _syncProgress.value = "同步失败: $fileName" } override fun onSyncFinished() { Log.d(TAG, "文件同步完成: $fileName") _syncProgress.value = "同步完成: $fileName" } } val success = cxrApi.syncSingleFile(localStorageDir.absolutePath, mediaType, fileName, syncCallback) Log.d(TAG, "同步单个文件结果: $success") success } catch (e: Exception) { Log.e(TAG, "同步单个文件异常", e) false } } /** * 开始同步所有文件 */ private fun startSyncAllFiles(types: Array<ValueUtil.CxrMediaType>): Boolean { return try { val syncCallback = object : SyncStatusCallback { override fun onSyncStart() { Log.d(TAG, "开始同步所有文件") _syncProgress.value = "开始同步所有文件..." } override fun onSingleFileSynced(fileName: String?) { Log.d(TAG, "文件同步成功: $fileName") _syncProgress.value = "同步成功: $fileName" } override fun onSyncFailed() { Log.e(TAG, "同步失败") _syncProgress.value = "同步失败" _isSyncing.value = false } override fun onSyncFinished() { Log.d(TAG, "所有文件同步完成") _syncProgress.value = "所有文件同步完成" _isSyncing.value = false _statusMessage.value = "所有文件同步完成" } } val success = cxrApi.startSync(localStorageDir.absolutePath, types, syncCallback) Log.d(TAG, "开始同步所有文件结果: $success") success } catch (e: Exception) { Log.e(TAG, "开始同步所有文件异常", e) false } } /** * 初始化 Wi-Fi 连接 */ private suspend fun initWifiConnection(): Boolean { return try { _isWifiConnecting.value = true val wifiCallback = object : WifiP2PStatusCallback { override fun onConnected() { Log.d(TAG, "Wi-Fi P2P 连接成功") _isWifiConnected.value = true _isWifiConnecting.value = false _statusMessage.value = "Wi-Fi 连接成功" } override fun onDisconnected() { Log.d(TAG, "Wi-Fi P2P 连接断开") _isWifiConnected.value = false _isWifiConnecting.value = false _statusMessage.value = "Wi-Fi 连接断开" } override fun onFailed(errorCode: ValueUtil.CxrWifiErrorCode?) { Log.e(TAG, "Wi-Fi P2P 连接失败: $errorCode") _isWifiConnected.value = false _isWifiConnecting.value = false val errorMessage = when (errorCode) { ValueUtil.CxrWifiErrorCode.WIFI_DISABLED -> "手机 Wi-Fi 未打开" ValueUtil.CxrWifiErrorCode.WIFI_CONNECT_FAILED -> "P2P 连接失败" ValueUtil.CxrWifiErrorCode.UNKNOWN -> "未知错误" else -> "连接失败" } _statusMessage.value = "Wi-Fi 连接失败: $errorMessage" } } val status = cxrApi.initWifiP2P(wifiCallback) when (status) { ValueUtil.CxrStatus.REQUEST_SUCCEED -> { Log.d(TAG, "Wi-Fi 初始化请求成功") true } ValueUtil.CxrStatus.REQUEST_WAITING -> { Log.d(TAG, "Wi-Fi 初始化请求等待中") _statusMessage.value = "Wi-Fi 初始化中..." true } ValueUtil.CxrStatus.REQUEST_FAILED -> { Log.e(TAG, "Wi-Fi 初始化请求失败") _statusMessage.value = "Wi-Fi 初始化失败" false } else -> { Log.e(TAG, "Wi-Fi 初始化请求状态未知: $status") _statusMessage.value = "Wi-Fi 初始化状态未知" false } } } catch (e: Exception) { Log.e(TAG, "初始化 Wi-Fi 连接异常", e) _statusMessage.value = "Wi-Fi 初始化异常: ${e.message}" _isWifiConnecting.value = false false } } /** * 获取 Wi-Fi 连接状态 */ fun getWifiConnectionStatus(): Boolean { return try { val isConnected = cxrApi.isWifiP2PConnected _isWifiConnected.value = isConnected isConnected } catch (e: Exception) { Log.e(TAG, "获取 Wi-Fi 连接状态失败", e) false } } /** * 反初始化 Wi-Fi 连接 */ fun deinitWifi() { try { cxrApi.deinitWifiP2P() _isWifiConnected.value = false _isWifiConnecting.value = false _statusMessage.value = "Wi-Fi 连接已断开" Log.d(TAG, "Wi-Fi 连接已反初始化") } catch (e: Exception) { Log.e(TAG, "反初始化 Wi-Fi 连接失败", e) _statusMessage.value = "断开 Wi-Fi 连接失败: ${e.message}" } } /** * 清理资源 */ override fun onCleared() { super.onCleared() try { // 移除媒体文件更新监听器 cxrApi.setMediaFilesUpdateListener(null) // 停止同步 cxrApi.stopSync() // 反初始化 Wi-Fi 连接 cxrApi.deinitWifiP2P() Log.d(TAG, "DeviceFilesViewModel 资源已清理") } catch (e: Exception) { Log.e(TAG, "清理资源失败", e) } }}
复制代码


进入页面调用 refreshDeviceFiles 获取设备文件,点击下载按钮调用 downloadFile 方法将文件从设备传输到手机,设备传输依赖 wifi 模块,所以在 downloadFile 方法中需要先连接 wifi 然后再下载文件。


**避坑指南:**wifi 是高耗电模块,使用完成及时关闭,避免设备耗电太快。

构建打包

开发完成后将代码打包构建,编译最终 apk 后即可安装到手机。首先在 gradle 中配置秘钥,在android节点下添加signingConfigs,配置密钥库路径、密码、别名等信息;然后在buildTypesrelease类型中引用该签名配置。接着点击顶部菜单栏 Build → Generate Signed Bundle/APK,选择“APK”,点击“Next”;接着选择 Release 构建类型,指定 APK 输出路径,点击“Finish”;构建完成后,Android Studio 会弹出提示框,显示 APK 文件的保存路径(默认路径为app/build/outputs/apk/release/app-release.apk)。


完成构建后将 apk 安装到手机就可以跟 Rokid Glasses 设备通信了,点击连接后可以开启录音,点击结束录音后可以将设备中录制的语音导出到手机,结合自身业务需求将音频传输到云端做 ASR 与用户画像分析等。

总结

Rokid Glasses 通过 CXR-M SDK 为开发者提供了完整的移动端控制与协同开发框架,涵盖蓝牙发现、连接、录音、拍照、录像、文件同步及 Wi-Fi 高速传输等核心能力。借助官方 SDK,企业或个人可在数小时内完成「手机-眼镜」端到端原型,将第一视角音视频、AI 助手、实时提词、会议纪要等场景快速落地。整套方案兼顾低功耗蓝牙的便捷与高带宽 Wi-Fi 的效率,本文结合自身业务将 Rokid Glasses 能力无缝接入自有业务后台与大模型 pipeline。随着 Rokid 生态持续迭代,开发者只需聚焦上层业务创新,即可让「长在眼前的 AI 助手」成为销售、培训、巡检、售后等场景的提效利器。</br>


![[Config/Templates/公众号介绍|公众号介绍]]


![[公众号卡片]]![[公众号欢迎词]]

发布于: 2025-10-13阅读数: 4
用户头像

轻口味

关注

🏆2021年InfoQ写作平台-签约作者 🏆 2017-10-17 加入

Android、音视频、AI相关领域从业者。 欢迎加我微信wodekouwei拉您进InfoQ音视频沟通群 邮箱:qingkouwei@gmail.com

评论

发布
暂无评论
Rokid Glasses 移动端控制应用开发初体验-助力业务创新_android_轻口味_InfoQ写作社区