动态

详情 返回 返回

新手上手:Rokid 移動端 + 眼鏡端最小實踐 - 动态 详情

新手上手:Rokid 移動端 + 眼鏡端最小實踐

每個開發,都喜歡逛論壇。我就是在逛論壇的時候,發現了Rokid,好吧,我承認我對這玩意很感興趣。稍微看了下Rokid的開發文檔,CXR-S SDK 和CXR-M SDK 分別是眼鏡端和移動端的SDK,居然有Kotlin的寫法,恰好我居然是寫Android的,真是太巧合了,拿着文檔啃了起來。

如果您有任何疑問、對文章寫的不滿意、發現錯誤或者有更好的方法,如果你想支持下一期請務必點贊~,歡迎在評論、私信或郵件中提出,非常感謝您的支持。🙏

那麼你將獲得

  • 端到端路徑:手機→ 眼鏡端顯示
  • 可直接複製的 Kotlin 代碼片段(移動端與眼鏡端)
  • 高頻踩坑與排錯清單

一、總體流程

我們看看總體流程,也是非常的清晰~~

從移動端初始化到眼鏡端顯示,最小閉環分為四步:

1) 初始化與權限:應用啓動 → 動態申請藍牙/定位/網絡權限 → 打開 BLE/Wi‑Fi。
2) 發現與連接:移動端 BLE 掃描(按 Rokid 服務 UUID 過濾)→ 選擇設備 → 通過 CXR-M 建鏈。
3) 消息通道:移動端建立 CXR 通道併發送統一格式的消息(如字符串/JSON)。
4) 渲染展示:眼鏡端訂閲相同消息名 → 解析載荷 → 更新 UI 渲染到顯示面板。

Untitled diagram _ Mermaid Chart-2025-09-25-125822


二、消息交互

那麼重中之重就是兩者的交互了,從下面的圖,我們可以很容易的瞭解整個消息交互的流程。

Untitled diagram _ Mermaid Chart-2025-09-25-125849

And 在整個流程中

  • 建議以業務語義命名,如 glass_text, capture_ctrl
  • 名稱兩端統一且固定,不在名字中混入環境/灰度信息;
  • 一類業務一個通道,便於限流與排障。

三、移動端(Android/Kotlin)

前置:確保在 Gradle 中加入 Rokid Maven 倉庫,按官方文檔導入 CXR-M 相關依賴,並在 AndroidManifest.xml 聲明藍牙/定位/網絡權限;Android 12+ 需要 BLUETOOTH_SCAN/BLUETOOTH_CONNECT

記得申請權限!!!

1) 動態權限(藍牙/定位/網絡)

private fun requestPermissions(activity: Activity) {
    val permissions = buildList {
        add(Manifest.permission.ACCESS_FINE_LOCATION)
        add(Manifest.permission.ACCESS_COARSE_LOCATION)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            add(Manifest.permission.BLUETOOTH_SCAN)
            add(Manifest.permission.BLUETOOTH_CONNECT)
        } else {
            add(Manifest.permission.BLUETOOTH)
            add(Manifest.permission.BLUETOOTH_ADMIN)
        }
        add(Manifest.permission.ACCESS_WIFI_STATE)
        add(Manifest.permission.CHANGE_WIFI_STATE)
        add(Manifest.permission.INTERNET)
    }.toTypedArray()
    ActivityCompat.requestPermissions(activity, permissions, 1001)
}

2) 設備掃描(用 UUID 過濾 Rokid 設備)

   private fun startBleScan(adapter: BluetoothAdapter, onFound: (BluetoothDevice) -> Unit) {
        val scanner = adapter.bluetoothLeScanner ?: return
        val filters = listOf(
            ScanFilter.Builder()
                .setServiceUuid(ParcelUuid.fromString(ROKID_SERVICE_UUID))
                .build()
        )
        val settings = ScanSettings.Builder().build()
        
        scanner.startScan(filters, settings, scanCallback)
        tvStatus.text = "正在掃描 Rokid 設備..."
        btnScan.text = "掃描中..."
        btnScan.isEnabled = false
    }

3) 建立藍牙連接

    private fun connectRokid(context: Context, device: BluetoothDevice) {
        CxrApi.getInstance().initBluetooth(context, device, object : BluetoothStatusCallback {
            override fun onConnectionInfo(
                socketUuid: String?, macAddress: String?, rokidAccount: String?, glassesType: Int
            ) {
                if (socketUuid != null && macAddress != null) {
                    CxrApi.getInstance().connectBluetooth(context, socketUuid, macAddress, this)
                }
            }
            override fun onConnected() { 
                Log.d(TAG, "Connected")
                runOnUiThread { 
                    tvStatus.text = "已連接到 Rokid 設備"
                    btnSend.isEnabled = true
                    btnScan.text = "連接成功"
                    btnScan.isEnabled = false
                }
            }
            override fun onDisconnected() { 
                Log.d(TAG, "Disconnected")
                runOnUiThread {
                    tvStatus.text = "設備已斷開連接"
                    btnSend.isEnabled = false
                    btnScan.text = "重新掃描"
                    btnScan.isEnabled = true
                }
            }
            override fun onFailed(code: ValueUtil.CxrBluetoothErrorCode?) {
                Log.e(TAG, "Failed: ${code}")
                runOnUiThread {
                    tvStatus.text = "連接失敗: $code"
                    btnSend.isEnabled = false
                    btnScan.text = "重新掃描"
                    btnScan.isEnabled = true
                }
            }
        })
    }

4) 發送消息

    private fun sendToGlasses(channel: String = "glass_test", content: String = "Hello Rokid") {
        val caps = Caps().apply {
            write("TEXT_UPDATE") // 子命令,眼鏡端按需解析
            write(content)        // 直接發送字符串
        }
        val status = CxrApi.getInstance().sendCustomCmd(channel, caps)
        runOnUiThread {
            tvStatus.text = "發送簡單消息: '$content', 狀態: $status"
        }
    }
注:請在移動端與眼鏡端統一消息名與載荷格式!!!!!!!!這很重要啊!

4)完整代碼

import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.bluetooth.*
import android.bluetooth.le.*
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.ParcelUuid
import android.util.Log
import android.widget.Button
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.gson.Gson
import com.rokid.cxr.Caps
import com.rokid.cxr.client.extend.CxrApi
import com.rokid.cxr.client.extend.callbacks.BluetoothStatusCallback
import com.rokid.cxr.client.utils.ValueUtil
import java.util.*

/**
 * 移動端連接Activity - 基於文章"新手上手:Rokid移動端+眼鏡端最小實踐"的源代碼實現
 * 
 * 對應文章章節:三、移動端(Android/Kotlin)
 * - 1) 動態權限(藍牙/定位/網絡)
 * - 2) 設備掃描(用 UUID 過濾 Rokid 設備)  
 * - 3) 建立藍牙連接
 * - 4) 發送消息(示例:字符串載荷)
 */
class ConnectActivity : AppCompatActivity() {
    companion object {
        private const val TAG = "ConnectActivity"
        private const val CHANNEL_TEXT = "glass_text"
        private const val CHANNEL_TEST = "glass_test"
        private const val ROKID_SERVICE_UUID = "00009100-0000-1000-8000-00805f9b34fb"
    }

    private lateinit var tvStatus: TextView
    private lateinit var btnScan: Button
    private lateinit var btnSend: Button
    private var bluetoothAdapter: BluetoothAdapter? = null
    private var bluetoothLeScanner: BluetoothLeScanner? = null
    private var connectedDevice: BluetoothDevice? = null
    private val gson = Gson()

    // 權限請求回調
    private val requestPermissionsLauncher = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissions ->
        val allGranted = permissions.all { it.value }
        if (allGranted) {
            initBluetooth()
        } else {
            tvStatus.text = "權限未授予,無法進行藍牙掃描"
        }
    }

    // 文章中的掃描回調示例
    @SuppressLint("MissingPermission")
    private val scanCallback = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            result.device?.let { device ->
                Log.d(TAG, "發現設備: ${device.name} - ${device.address}")
                tvStatus.text = "發現設備: ${device.name ?: "未知設備"}"
                scanAndConnect(device)
                stopScan()
            }
        }

        override fun onScanFailed(errorCode: Int) {
            Log.e(TAG, "掃描失敗: $errorCode")
            tvStatus.text = "掃描失敗: $errorCode"
            btnScan.text = "重新掃描"
            btnScan.isEnabled = true
        }
    }

    @SuppressLint("MissingInflatedId")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_connect)

        tvStatus = findViewById(R.id.tvStatus)
        btnScan = findViewById(R.id.btnScan)
        btnSend = findViewById(R.id.btnSend)

        btnScan.setOnClickListener { startBluetoothScan() }
        btnSend.setOnClickListener { sendTestMessage() }

        btnSend.isEnabled = false
        
        // 檢查並請求權限
        checkAndRequestPermissions()
    }

    private fun checkAndRequestPermissions() {
        requestPermissions(this)
    }

    // 文章中的動態權限申請示例
    private fun requestPermissions(activity: Activity) {
        val permissions = buildList {
            add(Manifest.permission.ACCESS_FINE_LOCATION)
            add(Manifest.permission.ACCESS_COARSE_LOCATION)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                add(Manifest.permission.BLUETOOTH_SCAN)
                add(Manifest.permission.BLUETOOTH_CONNECT)
            } else {
                add(Manifest.permission.BLUETOOTH)
                add(Manifest.permission.BLUETOOTH_ADMIN)
            }
            add(Manifest.permission.ACCESS_WIFI_STATE)
            add(Manifest.permission.CHANGE_WIFI_STATE)
            add(Manifest.permission.INTERNET)
        }.toTypedArray()
        
        val needsPermission = permissions.any { permission ->
            ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED
        }

        if (needsPermission) {
            ActivityCompat.requestPermissions(activity, permissions, 1001)
        } else {
            initBluetooth()
        }
    }

    private fun initBluetooth() {
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
        if (bluetoothAdapter == null) {
            tvStatus.text = "設備不支持藍牙"
            return
        }

        if (!bluetoothAdapter!!.isEnabled) {
            tvStatus.text = "請開啓藍牙"
            return
        }

        bluetoothLeScanner = bluetoothAdapter!!.bluetoothLeScanner
        tvStatus.text = "藍牙初始化完成,點擊掃描按鈕開始掃描"
    }

    @SuppressLint("MissingPermission")
    private fun startBluetoothScan() {
        // 文章中的設備掃描示例(用 UUID 過濾 Rokid 設備)
        if (bluetoothAdapter != null) {
            startBleScan(bluetoothAdapter!!) { device ->
                Log.d(TAG, "發現設備: ${device.name} - ${device.address}")
                tvStatus.text = "發現設備: ${device.name ?: "未知設備"}"
                scanAndConnect(device)
                stopScan()
            }
        }
    }

    // 文章中的設備掃描(用 UUID 過濾 Rokid 設備)函數
    @SuppressLint("MissingPermission")
    private fun startBleScan(adapter: BluetoothAdapter, onFound: (BluetoothDevice) -> Unit) {
        val scanner = adapter.bluetoothLeScanner ?: return
        val filters = listOf(
            ScanFilter.Builder()
                .setServiceUuid(ParcelUuid.fromString(ROKID_SERVICE_UUID))
                .build()
        )
        val settings = ScanSettings.Builder().build()
        
        scanner.startScan(filters, settings, scanCallback)
        tvStatus.text = "正在掃描 Rokid 設備..."
        btnScan.text = "掃描中..."
        btnScan.isEnabled = false
    }
    @SuppressLint("MissingPermission")
    private fun scanAndConnect(device: BluetoothDevice) {
        connectedDevice = device
        Log.d(TAG, "嘗試連接設備: ${device.address}")
        tvStatus.text = "正在連接 ${device.name ?: device.address}..."
        
        // 文章中的藍牙連接示例
        connectRokid(this, device)
    }

    // 文章中的建立藍牙連接示例
    private fun connectRokid(context: Context, device: BluetoothDevice) {
        CxrApi.getInstance().initBluetooth(context, device, object : BluetoothStatusCallback {
            override fun onConnectionInfo(
                socketUuid: String?, macAddress: String?, rokidAccount: String?, glassesType: Int
            ) {
                if (socketUuid != null && macAddress != null) {
                    CxrApi.getInstance().connectBluetooth(context, socketUuid, macAddress, this)
                }
            }
            override fun onConnected() { 
                Log.d(TAG, "Connected")
                runOnUiThread { 
                    tvStatus.text = "已連接到 Rokid 設備"
                    btnSend.isEnabled = true
                    btnScan.text = "連接成功"
                    btnScan.isEnabled = false
                }
            }
            override fun onDisconnected() { 
                Log.d(TAG, "Disconnected")
                runOnUiThread {
                    tvStatus.text = "設備已斷開連接"
                    btnSend.isEnabled = false
                    btnScan.text = "重新掃描"
                    btnScan.isEnabled = true
                }
            }
            override fun onFailed(code: ValueUtil.CxrBluetoothErrorCode?) {
                Log.e(TAG, "Failed: ${code}")
                runOnUiThread {
                    tvStatus.text = "連接失敗: $code"
                    btnSend.isEnabled = false
                    btnScan.text = "重新掃描"
                    btnScan.isEnabled = true
                }
            }
        })
    }

    private fun sendTestMessage() {
        // 使用文章中的示例代碼
        sendToGlasses(channel = "glass_test", content = "Hello Rokid")
        
        // 同時發送信封格式的消息示例
        val body = mapOf("text" to "Hello Rokid from Envelope")
        sendEnvelope(channel = CHANNEL_TEXT, body = body, type = "TEXT_UPDATE")
    }

    // 發送字符串載荷 (文章示例)
    private fun sendToGlasses(channel: String = "glass_test", content: String = "Hello Rokid") {
        val caps = Caps().apply {
            write("TEXT_UPDATE") // 子命令,眼鏡端按需解析
            write(content)        // 直接發送字符串
        }
        val status = CxrApi.getInstance().sendCustomCmd(channel, caps)
        runOnUiThread {
            tvStatus.text = "發送簡單消息: '$content', 狀態: $status"
        }
    }

    // 發送信封格式的消息(文章示例)
    private fun sendEnvelope(channel: String, body: Any, type: String) {
        val env = mapOf(
            "version" to "1.0",
            "type" to type,
            "seqId" to System.nanoTime(),
            "timestamp" to System.currentTimeMillis(),
            "traceId" to UUID.randomUUID().toString(),
            "body" to body
        )
        val json = gson.toJson(env)

        // 通過 CxrApi 的自定義指令通道發送:
        // onValueUpdate 的默認分支會把 (channel, caps) 投遞給眼鏡端的自定義命令監聽
        val caps = Caps().apply {
            write(type)   // 子命令/事件名,例如 "TEXT_UPDATE"
            write(json)   // 載荷(字符串/JSON)
        }
        val status = CxrApi.getInstance().sendCustomCmd(channel, caps)
        runOnUiThread {
            tvStatus.text = "發送信封消息: '$type', 狀態: $status"
        }
    }

    @SuppressLint("MissingPermission")
    private fun stopScan() {
        bluetoothLeScanner?.stopScan(scanCallback)
        btnScan.text = "重新掃描"
        btnScan.isEnabled = true
    }

    override fun onDestroy() {
        super.onDestroy()
        stopScan()
        if (connectedDevice != null) {
            CxrApi.getInstance().deinitBluetooth()
        }
    }
}

四、眼鏡端

眼鏡端基於 CXR-S SDK,需要在眼鏡設備上運行,其實也適合移動端是一樣的思路!

1) 項目配置與依賴

build.gradle.kts 中添加 CXR-S 依賴:

dependencies {
    implementation("com.rokid.cxr:cxr-service-bridge:1.0-20250519.061355-45")

}

2) 最小工作示例(MainActivity)

import android.os.Bundle
import android.util.Log
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.example.myapplication.R
import com.google.gson.Gson
import com.rokid.cxr.Caps
import com.rokid.cxr.client.extend.CxrApi
import com.rokid.cxr.client.extend.listeners.CustomCmdListener

class GlassesMainActivity : AppCompatActivity() {
    companion object {
        private const val TAG = "GlassesMain"
        private const val CHANNEL_TEXT = "glass_text"
        private const val CHANNEL_TEST = "glass_test"
    }

    private lateinit var tvMessage: TextView
    private lateinit var tvStatus: TextView
    private val gson = Gson()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setupUi()
        setupMessageListener()
        logStatus("眼鏡端已啓動,等待移動端連接...")
    }

    private fun setupUi() {
        setContentView(R.layout.activity_glasses_main)
        tvMessage = findViewById(R.id.tvMessage)
        tvStatus = findViewById(R.id.tvStatus)
    }

    private fun setupMessageListener() {
        // 設置自定義命令監聽器,處理來自移動端的消息
        CxrApi.getInstance().setCustomCmdListener(object : CustomCmdListener {
            override fun onCustomCmd(name: String, args: Caps?) {
                Log.d(TAG, "收到命令: channel=$name, args.size=${args?.size()}")
                
                when (name) {
                    CHANNEL_TEXT -> handleTextUpdate(args)
                    CHANNEL_TEST -> handleTestMessage(args)
                    else -> Log.w(TAG, "未知通道: $name")
                }
            }
        })
    }

    private fun handleTextUpdate(caps: Caps?) {
        if (caps == null || caps.size() < 2) {
            Log.e(TAG, "handleTextUpdate: caps 格式錯誤")
            return
        }

        try {
            // 按發送端寫入順序解析:
            // 第1個:子命令 (如 "TEXT_UPDATE")
            // 第2個:載荷 (JSON字符串)
            val subCommand = caps.at(0).getString()
            val payloadJson = caps.at(1).getString()
            
            Log.d(TAG, "子命令: $subCommand, 載荷: $payloadJson")

            // 解析 JSON 載荷
            val jsonMap = gson.fromJson(payloadJson, Map::class.java) as? Map<String, Any?>
            val messageBody = jsonMap?.get("body") as? Map<String, Any?>
            val text = messageBody?.get("text") as? String

            if (!text.isNullOrEmpty()) {
                updateMessage("文本更新: $text")
            } else {
                updateMessage("收到的載荷格式不正確")
            }
        } catch (e: Exception) {
            Log.e(TAG, "解析文本更新失敗", e)
            updateMessage("解析出錯: ${e.message}")
        }
    }

    private fun handleTestMessage(caps: Caps?) {
        if (caps == null || caps.size() < 2) {
            Log.e(TAG, "handleTestMessage: caps 格式錯誤")
            return
        }

        // 簡化處理,直接使用第二個寫入的內容
        val content = caps.at(1).getString()
        updateMessage("測試消息: $content")
    }

    private fun updateMessage(message: String) {
        Log.d(TAG, "更新UI: $message")
        runOnUiThread {
            tvMessage.text = message
            
            // 可選:回覆 ACK 給移動端
            sendAckToMobile(message)
        }
    }

    private fun updateStatus(status: String) {
        Log.d(TAG, "狀態更新: $status")
        runOnUiThread {
            tvStatus.text = status
        }
    }

    private fun logStatus(status: String) {
        Log.i(TAG, status)
        updateStatus(status)
    }

    // 發送 ACK 回覆給移動端(可選)
    private fun sendAckToMobile(originalMessage: String) {
        try {
            val ackJson = gson.toJson(mapOf(
                "code" to 0,
                "message" to "ACK",
                "original" to originalMessage,
                "timestamp" to System.currentTimeMillis()
            ))
            
            val caps = Caps().apply {
                write("ACK")
                write(ackJson)
            }
            
            CxrApi.getInstance().sendCustomCmd("glass_ack", caps)
            Log.d(TAG, "已發送 ACK")
        } catch (e: Exception) {
            Log.e(TAG, "發送 ACK 失敗", e)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 清理監聽器
        CxrApi.getInstance().setCustomCmdListener(null)
        Log.d(TAG, "眼鏡端已銷燬")
    }
}

3) 佈局文件 (activity_glasses_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/tvStatus"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="眼鏡端狀態"
        android:textSize="14sp"
        android:textColor="#666"
        android:layout_marginBottom="8dp" />

    <TextView
        android:id="@+id/tvMessage"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="等待消息..."
        android:textSize="18sp"
        android:textColor="#333"
        android:background="#f5f5f5"
        android:padding="12dp"
        android:layout_marginTop="8dp" />

</LinearLayout>

4) AndroidManifest.xml 配置

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/Theme.MyApplication">

    <activity
        android:name=".GlassesMainActivity"
        android:exported="true"
        android:screenOrientation="landscape">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

</application>

5) 擴展功能

眼鏡端還可以處理更多場景:

class GlassesMainActivity : AppCompatActivity() {
    // ... 前面的代碼 ...

    private fun setupExtendedListeners() {
        // 電量監聽
        CxrApi.getInstance().setBatteryLevelUpdateListener { level, isCharging ->
            updateStatus("電量: $level%, 充電狀態: ${if (isCharging) "充電中" else "未充電"}")
        }

        // 亮度監聽
        CxrApi.getInstance().setBrightnessUpdateListener { brightness ->
            updateStatus("當前亮度: $brightness")
        }

        // 音量監聽
        CxrApi.getInstance().setVolumeUpdateListener { volume ->
            updateStatus("當前音量: $volume")
        }
    }

    private fun demonstrateSceneControl() {
        // 控制特定場景(如翻譯、提詞器等)
        CxrApi.getInstance().controlScene(
            ValueUtil.CxrSceneType.TRANSLATION,
            true,  // 啓用
            null   // 額外參數
        )
        
        updateStatus("已啓用翻譯場景")
    }

    private fun sendStatusToMobile() {
        val status = mapOf(
            "battery" to 85,
            "brightness" to 50,
            "volume" to 30,
            "timestamp" to System.currentTimeMillis()
        )
        
        val caps = Caps().apply {
            write("STATUS_UPDATE")
            write(gson.toJson(status))
        }
        
        CxrApi.getInstance().sendCustomCmd("glass_status", caps)
    }
}

6) 常見眼鏡端 API 速覽

API 用途 示例
setCustomCmdListener() 接收移動端消息 核心交互通道
setBatteryLevelUpdateListener() 監聽電池狀態 監控電量變化
setBrightnessUpdateListener() 監聽亮度變化 UI 適配
controlScene() 控制應用場景 翻譯/提詞器/拍照
sendCustomCmd() 發送消息給移動端 狀態反饋/ACK

這樣就完成了一個完整可運行的眼鏡端示例,包含消息接收、解析、UI更新和狀態反饋功能。


五、把一切串起來(3 步走)

1) 移動端完成權限和連接;
2) 點擊按鈕發送消息(消息名與載荷格式與眼鏡端約定一致);
3) 眼鏡端訂閲同名通道,解析載荷並更新 UI。

建議先用最簡單的字符串載荷跑通,再逐步替換為結構化消息~~~


六、踩坑與排錯速查

在這個過程中,會遇到一些奇奇怪怪的問題,這邊我先把我遇到的問題提前分享給各位,

  • 發現不到設備:權限是否全部授予、BLE/Wi‑Fi 是否開啓、距離與干擾、固件版本
  • 回調不觸發:監聽註冊時機(onCreate/onStart)、生命週期銷燬、重複訂閲衝突
  • 消息發不出去/收不到:消息名不一致、載荷編碼不一致(JSON/Caps)、Wi‑Fi 未初始化
  • UI 不更新:未切回主線程、Activity 非前台、Fragment 生命週期影響
  • Android 12+:藍牙權限拆分(SCAN/CONNECT),注意清單與動態申請一致

七、進一步擴展

第一次上手兩端協同,看起來複雜,其實只要把“初始化—連接—消息—UI”四步逐一打通,就已完成 80% 的路徑。先小後大、先通後優;跑通最小閉環,再逐步加功能與體驗,你會更快抵達“可發佈”的成果。

而只要打通了這一步,和打通了任督二脈一樣。剩下的無非就是你發發,我發發的事情啦,比如做個天氣可視化啦~~~


八、結語

沒了。

如果您有任何疑問、對文章寫的不滿意、發現錯誤或者有更好的方法,如果你想支持下一期請務必點贊~,歡迎在評論、私信或郵件中提出,這對我真的很重要,非常感謝您的支持。🙏

Add a new 评论

Some HTML is okay.