前言
在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主要包含以下功能:
- 
扫描周围rokid glasses设备列表
 - 
连接某个设备
 - 
断开设备连接
 - 
停止扫描
 - 
开始录音
 - 
结束录音
 - 
下载glasses端录音文件到手机
 - 
删除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工程。
            
            
              scss
              
              
            
          
          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,还需要依赖它的依赖:
            
            
              erlang
              
              
            
          
          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 的设备。连接设备需要执行两步操作:
- 
扫描设备列表
 - 
选择设备连接
 
在我们页面中增加扫描设备、停止扫描按钮,以列表形式展示扫描到的设备列表,选中某个设备点击连接和进行手机与设备的蓝牙连接。
这里简单介绍下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实现如下:
            
            
              kotlin
              
              
            
          
          import android.Manifest
import android.annotation.SuppressLint
import android.app.Application
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.os.ParcelUuid
import android.util.Log
import androidx.annotation.RequiresPermission
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.qingkouwei.rokidclient.controller.RokidRecorderController
import com.qingkouwei.rokidclient.model.Device
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.Dispatchers
import java.util.concurrent.ConcurrentHashMap
import com.rokid.cxr.client.extend.CxrApi
import com.rokid.cxr.client.extend.callbacks.BluetoothStatusCallback
import 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设备就建立了连接,可以进行其他操作了。
避坑指南:
在开发手机应用与设备连接是基于蓝牙的,需要申请对应权限:
            
            
              xml
              
              
            
          
          <!-- 定位权限 -->  
<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实现代码如下:
            
            
              kotlin
              
              
            
          
          package com.qingkouwei.rokidclient.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.qingkouwei.rokidclient.controller.RokidRecorderController
import com.qingkouwei.rokidclient.model.AnalysisResult
import com.qingkouwei.rokidclient.model.Device
import com.qingkouwei.rokidclient.model.Recording
import com.qingkouwei.rokidclient.network.MockAnalysisService
import com.rokid.cxr.client.extend.CxrApi
import com.rokid.cxr.client.extend.callbacks.BluetoothStatusCallback
import com.rokid.cxr.client.utils.ValueUtil
import com.rokid.cxr.client.extend.listeners.AudioStreamListener
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.text.SimpleDateFormat
import 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代码实现如下:
            
            
              kotlin
              
              
            
          
          package com.qingkouwei.rokidclient.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.qingkouwei.rokidclient.controller.RokidRecorderController
import com.qingkouwei.rokidclient.model.DeviceFile
import com.qingkouwei.rokidclient.model.FileType
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import android.util.Log
// 导入 Rokid CXR SDK
import com.rokid.cxr.client.extend.CxrApi
import com.rokid.cxr.client.extend.callbacks.UnsyncNumResultCallback
import com.rokid.cxr.client.extend.listeners.MediaFilesUpdateListener
import com.rokid.cxr.client.extend.callbacks.SyncStatusCallback
import com.rokid.cxr.client.extend.callbacks.WifiP2PStatusCallback
import 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,配置密钥库路径、密码、别名等信息;然后在buildTypes的release类型中引用该签名配置。接着点击顶部菜单栏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 助手」成为销售、培训、巡检、售后等场景的提效利器。