如果你第一次接觸 Rokid 眼鏡生態,RokidDemo 就是你的“手機端和眼鏡端的橋”。它不是一個只能裝起來的示例,而是一個能把手機和眼鏡真正連在一起、做互動、做協同的基線工程。你能在它裏邊看到:
- 掃描發現 Rokid 眼鏡,並完成藍牙連接與鑑權
- 拉取眼鏡的狀態(電量、音量、亮度、充電)並在手機端展示與調節
- 打開眼鏡相機拍照並把圖片回傳到手機端保存與入庫
- 下發自定義界面到眼鏡端(比如彈出一個文字提示)
- 發送全局消息/TTS 反饋
- 模擬遙控器按鍵,通過 HID 報文控制眼鏡(方向、返回、音量)
- 把這些交互過程寫入數據庫,能分頁查看記錄
一句話:它就是你要做“手機+眼鏡”協同應用時的起跑線。項目代碼地址已經在github上了,這裏感謝作者的代碼樣例:https://github.com/StudiousXiaoYu/RokidDemo
安裝環境(Windows)
這個環節很關鍵,基本所有“構建失敗”“設備不識別”的坑都和環境有關。
安裝Kotlin
由於整個項目代碼環境是Kotlin環境,我們先本地IDE中安裝好相關插件。
安裝 Android Studio 和 OpenJDK 11
查看是否有安裝環境
winget search --source winget Android | Select-String -Pattern "Android Studio|SDK|Platform Tools|OpenJDK"
安裝OpenJDK.11
winget install -e --id Microsoft.OpenJDK.11 --accept-source-agreements --accept-package-agreements
安裝AndroidStudio
winget install -e --id Google.AndroidStudio --accept-source-agreements --accept-package-agreements
安裝PlatformTools
winget install -e --id Google.PlatformTools --accept-source-agreements --accept-package-agreements
自此基本環境就安裝完成了。
安裝 Android SDK 組件
確保安裝這些:
- Android SDK Platform 36(匹配項目
compileSdk 36) - Android SDK Build-Tools 35(或兼容版本)
- Android SDK Platform-Tools(包含 adb)
- Android SDK Command-line Tools (latest)(方便命令行管理)
啓動Android SDK Studio後,我們進入設置頁找到“SDK Tools”,找到相關的工具,點擊下載即可,如圖所示:
真機調試準備
手機開啓“開發者選項”和“USB 調試”。
第一次連接時,手機會彈窗問是否允許 USB 調試,請選擇允許並勾選“始終允許”。
Windows 某些設備會缺驅動,裝 Google USB Driver。確保終端輸入 adb devices 能看到設備。
項目技術細節
這一節把工程先跑起來,跟着做就能過構建。 先做三步:設置 local.properties → 添加 Rokid 倉庫 → 引入 client-m 依賴。
項目根目錄有 local.properties,需要把 sdk.dir 指向你機器的 Android SDK 路徑(常見是 C:\Users\你的用户名\AppData\Local\Android\Sdk)。
sdk.dir=C:\Users\yu\AppData\Local\Android\Sdk
倉庫與依賴(Gradle)
在頂層加倉庫,在模塊里加依賴,然後點 Gradle Sync。
// settings.gradle
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
maven { url 'https://maven.rokid.com/repository/maven-public' }
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url 'https://maven.rokid.com/repository/maven-public' }
}
}
// app/build.gradle
android {
defaultConfig {
minSdk = 28
}
}
dependencies {
implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2")
}
檢查:sdk.dir 路徑有效,依賴能解析,Sync 無報錯。
Manifest 權限與特性(Android 12+)
先把這些權限加到 AndroidManifest.xml,Android 12+ 還需要在運行時申請。
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
藍牙相關權限在連接前先申請;前台服務能避免進程被系統回收。
構建與安裝
現在把包編譯到真機上。 前置條件:adb devices 顯示設備為 device。
windows的命令腳本已經在項目裏了。
- 構建 Debug 包:
.\gradlew.bat assembleDebug -x lint
- 安裝到設備:
.\gradlew.bat installDebug
- 啓動入口 Activity:
adb shell am start -n com.blue.armobile/com.blue.glassesapp.feature.init.InitActivity
首次進入會彈權限提示,請授予存儲與通知權限以免初始化阻塞。
添加相關的權限後,我們就可以正常進入手機端的應用界面了,如圖所示:
設備鏈接
在開始之前,請先完成“SDK 導入”。我這裏使用的是藍牙掃描添加設備。
1 查找藍牙設備
package com.rokid.cxrandroiddocsample.helpers
// imports 省略:請確保已引入 Bluetooth*、Scan*、LiveData、ActivityResultContracts 等相關類
class BluetoothHelper(
val context: AppCompatActivity,
val initStatus: (INIT_STATUS) -> Unit,
val deviceFound: () -> Unit
) {
companion object {
const val TAG = "Rokid Glasses CXR-M"
const val REQUEST_CODE_PERMISSIONS = 100
// 必要權限:Android 12+ 需申請 BLUETOOTH_SCAN / BLUETOOTH_CONNECT
private val REQUIRED_PERMISSIONS = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
add(Manifest.permission.BLUETOOTH_SCAN)
add(Manifest.permission.BLUETOOTH_CONNECT)
}
}.toTypedArray()
// 初始化階段標記:用於引導 UI 與流程
enum class INIT_STATUS { NotStart, INITING, INIT_END }
}
val scanResultMap: ConcurrentHashMap<String, BluetoothDevice> = ConcurrentHashMap()
val bondedDeviceMap: ConcurrentHashMap<String, BluetoothDevice> = ConcurrentHashMap()
private var adapter: BluetoothAdapter? = null
private var manager: BluetoothManager? = null
// 從 Adapter 拿到 BLE Scanner;設備不支持時拋錯並提示授權
private val scanner by lazy {
adapter?.bluetoothLeScanner ?: run {
showRequestPermissionDialog()
throw Exception("Bluetooth is not supported!!")
}
}
@SuppressLint("MissingPermission")
// 監聽藍牙是否啓用:啓用後觸發掃描;未啓用則引導開啓
private val bluetoothEnabled: MutableLiveData<Boolean> = MutableLiveData<Boolean>().apply {
this.observe(context) {
if (this.value == true) {
initStatus.invoke(INIT_STATUS.INIT_END)
startScan()
} else {
showRequestBluetoothEnableDialog()
}
}
}
// 引導用户開啓系統藍牙開關
private val requestBluetoothEnable = context.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
adapter = manager?.adapter
} else {
showRequestBluetoothEnableDialog()
}
}
private var adapterSetter: BluetoothAdapter? = null
set(value) {
field = value
value?.let {
if (!it.isEnabled) {
requestBluetoothEnable.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
} else {
bluetoothEnabled.postValue(true)
}
}
}
private var managerSetter: BluetoothManager? = null
set(value) {
field = value
initStatus.invoke(INIT_STATUS.INITING)
value?.let { adapterSetter = it.adapter } ?: run { showRequestPermissionDialog() }
}
// 權限結果:成功則初始化 BluetoothManager;失敗彈權限提示
val permissionResult: MutableLiveData<Boolean> = MutableLiveData<Boolean>().apply {
this.observe(context) {
if (it == true) {
managerSetter = context.getSystemService(AppCompatActivity.BLUETOOTH_SERVICE) as BluetoothManager
} else {
showRequestPermissionDialog()
}
}
}
// 掃描回調:按設備名緩存,發現設備回調 UI 刷新
val scanListener = object : ScanCallback() {
@SuppressLint("MissingPermission")
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
result?.let { r ->
r.device.name?.let {
scanResultMap[it] = r.device
deviceFound.invoke()
}
}
}
override fun onScanFailed(errorCode: Int) {
}
}
fun checkPermissions() {
initStatus.invoke(INIT_STATUS.NotStart)
context.requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
context.registerReceiver(
bluetoothStateListener,
IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
)
}
@SuppressLint("MissingPermission")
fun release() {
context.unregisterReceiver(bluetoothStateListener)
stopScan()
permissionResult.postValue(false)
bluetoothEnabled.postValue(false)
}
private fun showRequestPermissionDialog() { }
private fun showRequestBluetoothEnableDialog() { }
@SuppressLint("MissingPermission")
@RequiresPermission(Manifest.permission.BLUETOOTH_SCAN)
fun startScan() {
scanResultMap.clear()
try {
// 使用 Service UUID 過濾 Rokid 眼鏡
scanner.startScan(
listOf(
ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("00009100-0000-1000-8000-00805f9b34fb"))
.build()
),
ScanSettings.Builder().build(),
scanListener
)
} catch (_: Exception) { }
}
@RequiresPermission(Manifest.permission.BLUETOOTH_SCAN)
fun stopScan() {
// 停止掃描,釋放回調
scanner.stopScan(scanListener)
}
val bluetoothStateListener = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val action = intent?.action
if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
if (state == BluetoothAdapter.STATE_OFF) {
initStatus.invoke(INIT_STATUS.NotStart)
bluetoothEnabled.postValue(false)
}
}
}
}
}
2 初始化藍牙獲取藍牙信息
fun initDevice(context: Context, device: BluetoothDevice){
// 通過 CXR 初始化藍牙模塊,監聽連接信息與狀態
CxrApi.getInstance().initBluetooth(context, device, object : BluetoothStatusCallback{
override fun onConnectionInfo(
socketUuid: String?,
macAddress: String?,
rokidAccount: String?,
glassesType: Int
) {
// 成功返回 socketUuid 與 macAddress,用於後續 connectBluetooth
socketUuid?.let { uuid ->
macAddress?.let { address->
connect(context, uuid, address)
}
}
}
// 藍牙已建立數據通道
override fun onConnected() { }
// 藍牙數據通道斷開
override fun onDisconnected() { }
// 初始化失敗,攜帶錯誤碼
override fun onFailed(p0: ValueUtil.CxrBluetoothErrorCode?) { }
})
}
3 連接藍牙模塊
fun connect(context: Context, socketUuid: String, macAddress: String){
// 使用初始化階段返回的 socketUuid 與 macAddress 建立藍牙通信鏈路
CxrApi.getInstance().connectBluetooth(context, socketUuid, macAddress, object : BluetoothStatusCallback{
override fun onConnectionInfo(
socketUuid: String?,
macAddress: String?,
rokidAccount: String?,
glassesType: Int
) { }
// 連接成功
override fun onConnected() { }
// 連接斷開
override fun onDisconnected() { }
// 連接失敗,查看錯誤碼定位問題
override fun onFailed(p0: ValueUtil.CxrBluetoothErrorCode?) { }
})
}
4 獲取藍牙通信模塊連接狀態
fun getConnectionStatus(): Boolean{
// true:已連接;false:未連接
return CxrApi.getInstance().isBluetoothConnected
}
5 反初始化藍牙
fun deInit(){
// 釋放藍牙通信模塊,清理內部狀態
CxrApi.getInstance().deinitBluetooth()
}
6 藍牙重連
fun reconnect(context: Context, socketUuid: String, macAddress: String){
// 斷開後使用同一 socketUuid 與 macAddress 嘗試重連
CxrApi.getInstance().connectBluetooth(context, socketUuid, macAddress, object : BluetoothStatusCallback{ })
}
Wi‑Fi 連接
fun initWifi(): ValueUtil.CxrStatus?{
// 初始化 Wi‑Fi P2P 通信模塊,藍牙鏈路建立後再使用
return CxrApi.getInstance().initWifiP2P(object : WifiP2PStatusCallback{
override fun onConnected() { /* Wi‑Fi P2P 建立成功 */ }
override fun onDisconnected() { /* Wi‑Fi P2P 斷開 */ }
override fun onFailed(errorCode: ValueUtil.CxrWifiErrorCode?) { /* 根據錯誤碼處理 */ }
})
}
fun getWiFiConnectionStatus(): Boolean{
// true:Wi‑Fi P2P 已連接;false:未連接
return CxrApi.getInstance().isWifiP2PConnected
}
private fun deinitWifi(){
// 釋放 Wi‑Fi P2P 模塊
CxrApi.getInstance().deinitWifiP2P()
}
- 掃描階段只做發現與過濾,關鍵是拿到設備標識(名稱、MAC 等)
- initBluetooth 返回 socketUuid 與 macAddress,這兩個是後續通信的入口參數
- 真正的數據鏈路在 connectBluetooth 建立,成功後才算可用
- 連接狀態用 isBluetoothConnected 判斷,斷開要及時做 UI 與重試處理
- Wi‑Fi P2P 作為大數據傳輸通道,建議在藍牙穩定後再開啓,避免耗電與不必要的失敗
拍照錄音
拍照(三種途徑)
- 單機按鍵拍照:先設置分辨率,照片存入未同步媒體文件
- AI 場景拍照:打開相機並拍照,經藍牙返回 WebP 字節
- 喚起相機拍照:直接拿到照片路徑,再做本地/同步處理
// 單機按鍵:設置拍照分辨率
CxrApi.getInstance().setPhotoParams(width, height)
// AI 場景:打開相機 + 拍照(quality: 0~100)
CxrApi.getInstance().openGlassCamera(width, height, quality)
CxrApi.getInstance().takeGlassPhoto(width, height, quality, photoResultCallback)
// 直接喚起相機:返回照片路徑
CxrApi.getInstance().takeGlassPhoto(width, height, quality, photoPathCallback)
photoResultCallback 返回狀態與 WebP 字節;photoPathCallback 返回狀態與存儲路徑。經藍牙傳輸時建議選較小分辨率與合適質量,避免耗時與失敗。
數據操作
數據操作前請確保設備處於藍牙連接狀態;同步媒體文件前需先初始化 Wi‑Fi 通信模塊。
向眼鏡端發送數據
val streamCallback = object : SendStatusCallback {
override fun onSendSucceed() {}
override fun onSendFailed(errorCode: ValueUtil.CxrSendErrorCode?) {}
}
CxrApi.getInstance().sendStream(ValueUtil.CxrStreamType.WORD_TIPS, bytes, fileName, streamCallback)
sendStream 用於向眼鏡端發送流數據(如提詞內容);成功與失敗通過回調告知,失敗可根據錯誤碼定位。
讀取未同步媒體文件數量
val unSyncCallback = object : UnsyncNumResultCallback {
override fun onUnsyncNumResult(status: ValueUtil.CxrStatus?, audioNum: Int, pictureNum: Int, videoNum: Int) {}
}
CxrApi.getInstance().getUnsyncNum(unSyncCallback)
返回音頻、圖片、視頻三類未同步數量,可用於提醒或批量同步的前置判斷。
監聽眼鏡端媒體文件更新
val mediaFileUpdateListener = object : MediaFilesUpdateListener { override fun onMediaFilesUpdated() {} }
CxrApi.getInstance().setMediaFilesUpdateListener(mediaFileUpdateListener)
當眼鏡端媒體庫有更新時觸發;需要時設置,不用時移除,降低資源佔用。
同步媒體文件(需 Wi‑Fi)
val syncCallback = object : SyncStatusCallback {
override fun onSyncStart() {}
override fun onSingleFileSynced(fileName: String?) {}
override fun onSyncFailed() {}
override fun onSyncFinished() {}
}
CxrApi.getInstance().startSync(savePath, arrayOf(ValueUtil.CxrMediaType.PICTURE, ValueUtil.CxrMediaType.VIDEO), syncCallback)
同時同步多類型文件;保存路徑需具備文件管理權限;開始與完成、單文件成功與失敗均在回調通知。
CxrApi.getInstance().syncSingleFile(savePath, ValueUtil.CxrMediaType.PICTURE, fileName, syncCallback)
CxrApi.getInstance().stopSync()
同步指定單個文件與停止同步;單文件更適合“選擇後同步”的場景。
常見問題與排障
- 依賴解析失敗 → 未配置 Rokid 倉庫 → 在 settings.gradle 添加 maven 倉庫
- 藍牙掃描失敗 → 未申請運行時權限 → 申請 BLUETOOTH_SCAN/CONNECT
- 鑑權失敗 → .lc 未打包或資源 ID 錯誤 → 放入 res/raw 並讀取校驗
- 構建錯誤 → compileSdk 與 SDK 不匹配 → 統一為 compileSdk 36
- 初始化卡住 → 未授予存儲/通知權限 → 在首次啓動時授予權限
總結
到這裏你已經把環境、依賴、權限都配好,能連上眼鏡並跑過音量/亮度、拍照、消息和數據記錄。在這篇文章中,我們詳細介紹瞭如何搭建與優化Rokid眼鏡和手機端的協同應用。通過提供的示例代碼和詳細的安裝步驟,讀者能夠快速上手,完成手機與眼鏡的連接,並實現音量調節、拍照、信息推送等基礎功能。關鍵的技術點包括環境配置、Android Studio的設置、以及如何進行設備連接與權限管理等。
通過實際的功能實現案例,如藍牙連接、TTS反饋和遠程控制等,我們進一步探討了如何利用CXR SDK實現眼鏡與手機的交互。這些功能不僅能幫助開發者更好地理解眼鏡端與手機端的通訊機制,還能夠為未來應用的擴展和優化奠定基礎。