博客 / 詳情

返回

從零跑起 RokidDemo:開發小白也能搞定的入門實踐

如果你第一次接觸 Rokid 眼鏡生態,RokidDemo 就是你的“手機端和眼鏡端的橋”。它不是一個只能裝起來的示例,而是一個能把手機和眼鏡真正連在一起、做互動、做協同的基線工程。你能在它裏邊看到:

  • 掃描發現 Rokid 眼鏡,並完成藍牙連接與鑑權
  • 拉取眼鏡的狀態(電量、音量、亮度、充電)並在手機端展示與調節
  • 打開眼鏡相機拍照並把圖片回傳到手機端保存與入庫
  • 下發自定義界面到眼鏡端(比如彈出一個文字提示)
  • 發送全局消息/TTS 反饋
  • 模擬遙控器按鍵,通過 HID 報文控制眼鏡(方向、返回、音量)
  • 把這些交互過程寫入數據庫,能分頁查看記錄

一句話:它就是你要做“手機+眼鏡”協同應用時的起跑線。項目代碼地址已經在github上了,這裏感謝作者的代碼樣例:https://github.com/StudiousXiaoYu/RokidDemo

安裝環境(Windows)

這個環節很關鍵,基本所有“構建失敗”“設備不識別”的坑都和環境有關。

安裝Kotlin

由於整個項目代碼環境是Kotlin環境,我們先本地IDE中安裝好相關插件。

image

安裝 Android Studio 和 OpenJDK 11

查看是否有安裝環境

winget search --source winget Android | Select-String -Pattern "Android Studio|SDK|Platform Tools|OpenJDK"

image

安裝OpenJDK.11

winget install -e --id Microsoft.OpenJDK.11 --accept-source-agreements --accept-package-agreements

image

安裝AndroidStudio

winget install -e --id Google.AndroidStudio --accept-source-agreements --accept-package-agreements

image

安裝PlatformTools

winget install -e --id Google.PlatformTools --accept-source-agreements --accept-package-agreements

image

自此基本環境就安裝完成了。

安裝 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”,找到相關的工具,點擊下載即可,如圖所示:

image

真機調試準備

手機開啓“開發者選項”和“USB 調試”。

image

第一次連接時,手機會彈窗問是否允許 USB 調試,請選擇允許並勾選“始終允許”。

Windows 某些設備會缺驅動,裝 Google USB Driver。確保終端輸入 adb devices 能看到設備。

項目技術細節

這一節把工程先跑起來,跟着做就能過構建。 先做三步:設置 local.properties → 添加 Rokid 倉庫 → 引入 client-m 依賴。

項目根目錄有 local.properties,需要把 sdk.dir 指向你機器的 Android SDK 路徑(常見是 C:\Users\你的用户名\AppData\Local\Android\Sdk)。

image

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的命令腳本已經在項目裏了。

  1. 構建 Debug 包:

.\gradlew.bat assembleDebug -x lint

  1. 安裝到設備:

.\gradlew.bat installDebug

  1. 啓動入口 Activity:

adb shell am start -n com.blue.armobile/com.blue.glassesapp.feature.init.InitActivity

首次進入會彈權限提示,請授予存儲與通知權限以免初始化阻塞。

image

添加相關的權限後,我們就可以正常進入手機端的應用界面了,如圖所示:

image

設備鏈接

在開始之前,請先完成“SDK 導入”。我這裏使用的是藍牙掃描添加設備。

image

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實現眼鏡與手機的交互。這些功能不僅能幫助開發者更好地理解眼鏡端與手機端的通訊機制,還能夠為未來應用的擴展和優化奠定基礎。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.