新手上手: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 渲染到显示面板。

二、消息交互

那么重中之重就是两者的交互了,从下面的图,我们可以很容易的了解整个消息交互的流程。

And 在整个流程中

  • 建议以业务语义命名,如 glass_text, capture_ctrl
  • 名称两端统一且固定,不在名字中混入环境/灰度信息;
  • 一类业务一个通道,便于限流与排障。

三、移动端(Android/Kotlin)

前置:确保在 Gradle 中加入 Rokid Maven 仓库,按官方文档导入 CXR-M 相关依赖,并在 AndroidManifest.xml 声明蓝牙/定位/网络权限;Android 12+ 需要 BLUETOOTH_SCAN/BLUETOOTH_CONNECT

记得申请权限!!!

1) 动态权限(蓝牙/定位/网络)

scss 复制代码
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 设备)

kotlin 复制代码
   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) 建立蓝牙连接

kotlin 复制代码
    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) 发送消息

kotlin 复制代码
    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)完整代码

kotlin 复制代码
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 依赖:

scss 复制代码
dependencies {
    implementation("com.rokid.cxr:cxr-service-bridge:1.0-20250519.061355-45")
​
}

2) 最小工作示例(MainActivity)

kotlin 复制代码
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)

ini 复制代码
<?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 配置

ini 复制代码
<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) 扩展功能

眼镜端还可以处理更多场景:

kotlin 复制代码
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% 的路径。先小后大、先通后优;跑通最小闭环,再逐步加功能与体验,你会更快抵达"可发布"的成果。

而只要打通了这一步,和打通了任督二脉一样。剩下的无非就是你发发,我发发的事情啦,比如做个天气可视化啦~~~


八、结语

没了。

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,如果你想支持下一期请务必点赞 ~,欢迎在评论、私信或邮件中提出,这对我真的很重要,非常感谢您的支持。🙏

相关推荐
前端达人3 小时前
「React实战面试题」:React.memo为什么失效了?
前端·javascript·react.js·前端框架·ecmascript
江城开朗的豌豆3 小时前
嘿,别想那么复杂!我的第一个微信小程序长这样
前端·javascript·微信小程序
Irene19913 小时前
URLSearchParams :处理 URL 查询参数的接口
开发语言·前端·javascript
Dontla3 小时前
Web典型路由结构之Next.js (App Router, v13+) )(文件系统驱动的路由:File-based Routing)声明式路由:文件即路由
开发语言·前端·javascript
我不是程序媛lisa3 小时前
前端正确处理“文字溢出”的思路与最佳实践
前端·css3
BigTopOne3 小时前
【Fragment】parentFragmentManager , childFragmentManager 区别是什么? 分别在什么场景使用?
前端
岁月宁静3 小时前
Vue3.5 + SSE 构建高可用 AI 聊天交互层 ——chat.js 模块架构与实现
前端·vue.js·人工智能
~无忧花开~3 小时前
JavaScript学习笔记(十七):ES6生成器函数详解
开发语言·前端·javascript·笔记·学习·es6·js
前端 贾公子3 小时前
Vue3 defineModel === 实现原理
前端·javascript·vue.js