基于 Rokid AI Glasses 的春节红包助手:从零到一的技术实践

举报
Xxtaoaooo 发表于 2026/03/03 00:20:30 2026/03/03
【摘要】 人们眼中的天才之所以卓越非凡,并非天资超人一等而是付出了持续不断的努力。1万小时的锤炼是任何人从平凡变成超凡的必要条件。———— 马尔科姆·格拉德威尔🌟 Hello,我是Xxtaoaooo!🌈 “代码是逻辑的诗篇,架构是思想的交响” 摘要在移动支付深度融入日常生活的今天,微信红包和支付宝红包已成为春节等节日中传递祝福的重要方式。然而,消息刷屏导致的红包漏看、跨应用的红包统计不便、金额记录...

人们眼中的天才之所以卓越非凡,并非天资超人一等而是付出了持续不断的努力。1万小时的锤炼是任何人从平凡变成超凡的必要条件。———— 马尔科姆·格拉德威尔

🌟 Hello,我是Xxtaoaooo!
🌈 “代码是逻辑的诗篇,架构是思想的交响”


摘要

在移动支付深度融入日常生活的今天,微信红包和支付宝红包已成为春节等节日中传递祝福的重要方式。然而,消息刷屏导致的红包漏看、跨应用的红包统计不便、金额记录混乱等问题,依然是许多用户的痛点
Rokid CXR-M SDK 为开发者提供了连接 Android 手机与 Rokid AI Glasses 的完整能力栈,涵盖蓝牙设备扫描连接、跨设备消息传输、状态同步等核心功能。本文围绕"红包智能管理"这一贴近生活的实用场景,设计并实现一套名为「春节红包助手」的 AR 辅助系统。
该系统通过 Android 无障碍服务(AccessibilityService)监听微信/支付宝的红包消息,自动提取金额与发送者信息并存储至本地数据库,同时通过 CXR-M SDK 将实时通知推送至眼镜端显示,支持语音查询红包统计数据。整个方案严格遵循 SDK 接口规范,在设备连接、消息监听、数据解析、语音交互等环节进行应用开发实践,旨在为开发者提供一个可复现、可落地、贴近生活的技术范本,展示Rokid AR 生态在日常场景中的应用潜力。

一、为什么做这个项目?

春节是中国人最重要的传统节日,而"红包"则是春节最具代表性的文化符号之一。记得去年春节,家族群里红包满天飞,但我总有几个痛点:

  1. 消息容易漏看——群里消息刷得快,红包往往被淹没
  2. 金额记不清——发了多少、收了多少,最后算账要翻半天记录
  3. 跨应用管理麻烦——微信、支付宝都有红包,统计起来很费劲

作为一个技术人,我就想:能不能用 AR 眼镜来解决这个问题?戴上眼镜,红包消息直接显示在眼前,还能自动记账、语音查询,岂不美哉?

说干就干。网上了解看了看 Rokid AR 眼镜,一直想折腾点有意思的东西,这个场景挺合适的,于是就动手了。


二、技术选型:为什么选 Rokid?

其实一开始我也看过几家 AR 眼镜的 SDK,最后选 Rokid 主要是因为:

  1. 文档还算能看——虽然有些地方写得有点简略,但照着做基本能跑起来
  2. 论坛里有人踩过坑——遇到问题去开发者论坛里面找一找,大概率能找到答案
  3. CXR-M/CXR-S 分离设计——移动端和眼镜端各干各的,逻辑清晰

整体技术栈大概是这样的:

图1:系统架构图 客户端、服务层、数据层的关系

架构说明:

层级 职责
客户端层 Rokid Glasses 负责显示提醒,Android Phone 负责逻辑处理
服务层 CXR-M/CXR-S SDK 处理跨设备通信,AccessibilityService 监听红包
数据层 Room Database 存储红包记录,SharedPreferences 存储配置

SDK 与Glasses结构

图2:SDK 与Glasses结构


三、SDK 初始化与配置

开始写代码前,得先把 SDK 的环境搭好。CXR-M SDK 是通过 Maven 仓库分发的,配置起来不复杂,但有几个地方容易踩坑。

3.1 添加 Maven 仓库

CXR SDK 托管在 Rokid 自家的 Maven 仓库里,需要在项目的 settings.gradle.kts 中添加仓库地址。

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        maven { url = uri("https://maven.rokid.com/repository/maven-public/") }
        google()
        maven.central()
    }
}

3.2 添加 SDK 依赖

app/build.gradle.kts 中添加 CXR-M SDK 依赖。注意 SDK 要求 minSdk 至少是 28。

android {
    compileSdk = 34

    defaultConfig {
        applicationId = "com.rokid.redpacket"
        minSdk = 28  // CXR-M SDK 最低要求
        targetSdk = 34
    }
}

dependencies {
    // CXR-M SDK 核心依赖
    implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2")

    // 网络通信相关(SDK 依赖)
    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("com.google.code.gson:gson:2.10.1")
}

几点注意:

  • Rokid 的 Maven 仓库国内访问速度还可以,但有些网络环境可能需要配代理
  • SDK 版本号会更新,建议用最新的稳定版
  • minSdk 28 是硬性要求,低于这个版本编译会报错

3.3 配置权限

CXR-M SDK 需要蓝牙和位置权限才能工作。在 AndroidManifest.xml 中声明:

<!-- 蓝牙权限 -->
<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.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />

Android 12+ 还需要动态申请蓝牙权限:

private val requiredPermissions = mutableListOf(
    Manifest.permission.ACCESS_FINE_LOCATION
).apply {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        add(Manifest.permission.BLUETOOTH_SCAN)
        add(Manifest.permission.BLUETOOTH_CONNECT)
    }
}.toTypedArray()

3.4 初始化 CXRClientM

SDK 初始化建议在 Application 中做,确保全局只有一个实例。

class RedPacketApplication : Application() {

    companion object {
        lateinit var cxrClient: CXRClientM
            private set
    }

    override fun onCreate() {
        super.onCreate()

        // 获取 CXRClientM 单例
        cxrClient = CXRClientM.getInstance()

        // 设置连接状态监听
        cxrClient.setConnectionStateListener(object : ConnectionStateListener {
            override fun onConnectionStateChanged(state: Int, deviceAddress: String?) {
                when (state) {
                    ConnectionStateListener.STATE_CONNECTED -> {
                        Log.d("CXR", "眼镜已连接: $deviceAddress")
                    }
                    ConnectionStateListener.STATE_DISCONNECTED -> {
                        Log.d("CXR", "眼镜已断开")
                    }
                    ConnectionStateListener.STATE_CONNECTING -> {
                        Log.d("CXR", "正在连接眼镜...")
                    }
                }
            }
        }, true)

        // 设置数据监听(接收眼镜发来的消息)
        cxrClient.setDataListener { data ->
            val message = String(data)
            Log.d("CXR", "收到消息: $message")
            // 处理眼镜发来的消息
        }
    }
}

注意点:

  • setConnectionStateListener 的第二个参数传 true 表示立即回调当前状态
  • setDataListener 返回的是 ByteArray,需要自己转成 String 或对象
  • 初始化时机要早,建议在 Application 的 onCreate 中做

3.5 蓝牙设备扫描与连接

CXR-M SDK 提供了设备连接方法,但蓝牙扫描需要自己实现。Rokid 设备有一个特征 UUID:00009100-0000-1000-8000-00805f9b34fb

class BluetoothScanner(private val context: Context) {

    private val ROKID_SERVICE_UUID = UUID.fromString("00009100-0000-1000-8000-00805f9b34fb")

    fun startScan(callback: (BluetoothDevice) -> Unit) {
        val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
        val bleScanner = bluetoothAdapter?.bluetoothLeScanner

        if (bleScanner == null) {
            Log.e("Bluetooth", "设备不支持 BLE")
            return
        }

        val scanCallback = object : ScanCallback() {
            override fun onScanResult(callbackType: Int, result: ScanResult) {
                val device = result.device
                val uuids = result.scanRecord?.serviceUuids

                // 检查是否是 Rokid 设备
                val isRokidDevice = uuids?.any {
                    it.uuid == ROKID_SERVICE_UUID
                } == true

                if (isRokidDevice) {
                    Log.d("Bluetooth", "发现 Rokid 设备: ${device.name}")
                    bleScanner.stopScan(this)
                    callback(device)
                }
            }

            override fun onScanFailed(errorCode: Int) {
                Log.e("Bluetooth", "扫描失败: $errorCode")
            }
        }

        // 开始扫描
        bleScanner.startScan(scanCallback)

        // 10秒后自动停止
        Handler(Looper.getMainLooper()).postDelayed({
            bleScanner.stopScan(scanCallback)
        }, 10000)
    }
}

连接设备:

fun connectDevice(device: BluetoothDevice) {
    RedPacketApplication.cxrClient.connectDevice(device, object : ConnectResultCallback {
        override fun onConnectResult(result: Boolean, deviceAddress: String?) {
            if (result) {
                Log.d("CXR", "连接成功: $deviceAddress")
            } else {
                Log.e("CXR", "连接失败")
            }
        }
    })
}

注意点:

  • 蓝牙扫描耗电,记得设置超时自动停止
  • 连接失败可以加入重试逻辑,但要注意间隔时间
  • 设备名称可能为空,判断 Rokid 设备要靠 UUID

四、核心功能实现

4.1 无障碍服务监听红包

先说核心思路。Android 有个叫 AccessibilityService 的东西,本来是给残障用户用的,但很多人用它来做自动化。它的原理很简单:监听系统界面变化事件。我们就能通过它,在微信/支付宝有红包消息到达时第一时间知道。

1. 配置无障碍服务

首先创建一个继承自 AccessibilityService 的类:

class RedPacketAccessibilityService : AccessibilityService() {
    companion object {
        private const val TAG = "RedPacketAccessibility"
    }

    private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    private lateinit var repository: RedPacketRepository

    override fun onCreate() {
        super.onCreate()
        instance = this
        Log.d(TAG, "无障碍服务已启动")

        val database = AppDatabase.getDatabase(applicationContext)
        repository = RedPacketRepository(database.redPacketDao())
    }

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {
        event ?: return

        when (event.eventType) {
            AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
            AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
                handleWindowEvent(event)
            }
        }
    }

    private fun handleWindowEvent(event: AccessibilityEvent) {
        val packageName = event.packageName?.toString() ?: return

        // 只监听微信和支付宝
        if (packageName !in listOf("com.tencent.mm", "com.eg.android.AlipayGphone")) {
            return
        }

        val rootNode = rootInActiveWindow ?: return
        traverseNodes(rootNode, packageName)
    }

    // ... 后续代码
}

然后在 res/xml/accessibility_service_config.xml 中配置:

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeWindowContentChanged|typeWindowStateChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:packageNames="com.tencent.mm,com.eg.android.AlipayGphone"
    android:description="@string/accessibility_service_description"
    android:notificationTimeout="100" />

注意点:

  • packageNames 属性限制了只监听微信和支付宝,避免不必要的性能开销
  • typeWindowContentChanged 事件类型可以捕获界面内容变化,包括新消息到达
  • 无障碍服务需要用户手动在系统设置中开启,记得在应用内做好引导

2. 解析红包信息

这部分最头疼。微信和支付宝的界面结构完全不一样,红包消息的 DOM 结构也不固定。我基本上是用 UI Automator Viewer 一个节点一个节点看的,花了不少时间才摸清规律。

private fun traverseNodes(
    node: AccessibilityNodeInfoCompat,
    packageName: String
) {
    val text = node.text?.toString()
        ?: node.contentDescription?.toString()
        ?: ""

    // 检查是否包含红包关键字
    if (text.contains("红包") || text.contains("恭喜发财") || text.contains("领取")) {
        val amount = extractAmount(text)
        if (amount != null && amount > 0) {
            // 防抖:避免重复记录
            val currentTime = System.currentTimeMillis()
            if (currentTime - lastPacketTime > 2000 || text != lastPacketContent) {
                lastPacketTime = currentTime
                lastPacketContent = text

                serviceScope.launch {
                    val source = extractSource(node, packageName)
                    val sourceType = if (packageName == "com.tencent.mm") {
                        SourceType.WECHAT
                    } else {
                        SourceType.ALIPAY
                    }

                    repository.addRedPacket(
                        type = PacketType.RECEIVE,
                        amount = amount,
                        source = source,
                        sourceType = sourceType
                    )

                    // 通知眼镜
                    notifyRedPacketToGlasses(amount, source, sourceType)
                }
            }
        }
    }

    // 递归遍历子节点
    for (i in 0 until node.childCount) {
        val child = node.getChild(i) ?: continue
        traverseNodes(child, packageName)
    }
}

3. 提取金额的正则表达式:

private fun extractAmount(text: String): Double? {
    // 匹配:¥200.00, 200元, 200.00等格式
    val amountPattern = Regex("""[¥¥]?(\d+\.?\d*)\s*[元]?""")
    val match = amountPattern.find(text) ?: return null

    return try {
        match.groupValues[1].toDouble()
    } catch (e: Exception) {
        null
    }
}

注意点:

  • 正则匹配金额时要注意边界情况,比如"恭喜发财"四个字就不应该被匹配
  • 防抖机制很重要,同一红包消息可能触发多次界面变化事件
  • 微信的发送者信息通常在红包消息的上方,需要向上遍历父节点查找

4.2 数据存储层实现

红包数据需要持久化存储,方便后续查询和统计。这里用的是 Google 官方的 Room 数据库,比原生 SQLite 好用不少。

1. 定义数据实体

@Entity(tableName = "red_packet_record")
data class RedPacketRecord(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,

    val type: PacketType,           // RECEIVE 或 SEND
    val amount: Double,              // 金额
    val source: String,              // 来源/去向
    val sourceType: SourceType,      // WECHAT 或 ALIPAY
    val remark: String? = null,      // 备注
    val timestamp: Long,             // 时间戳
    val isLuckyPacket: Boolean = false, // 是否拼手气红包
    val createdAt: Long = System.currentTimeMillis()
)

enum class PacketType {
    RECEIVE, SEND
}

enum class SourceType {
    WECHAT, ALIPAY
}

2. 创建 DAO 接口

@Dao
interface RedPacketDao {
    // 获取所有记录,按时间倒序
    @Query("SELECT * FROM red_packet_record ORDER BY timestamp DESC")
    fun getAllRecords(): Flow<List<RedPacketRecord>>

    // 获取收到的红包
    @Query("SELECT * FROM red_packet_record WHERE type = 'RECEIVE' ORDER BY timestamp DESC")
    fun getReceivedRecords(): Flow<List<RedPacketRecord>>

    // 获取发出的红包
    @Query("SELECT * FROM red_packet_record WHERE type = 'SEND' ORDER BY timestamp DESC")
    fun getSentRecords(): Flow<List<RedPacketRecord>>

    // 统计总收入
    @Query("SELECT SUM(amount) FROM red_packet_record WHERE type = 'RECEIVE'")
    suspend fun getTotalReceived(): Double?

    // 统计总支出
    @Query("SELECT SUM(amount) FROM red_packet_record WHERE type = 'SEND'")
    suspend fun getTotalSent(): Double?

    // 插入记录
    @Insert
    suspend fun insert(record: RedPacketRecord): Long
}

3. 构建 Database

@Database(
    entities = [RedPacketRecord::class],
    version = 1,
    exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun redPacketDao(): RedPacketDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "red_packet_database"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

注意点:

  • Room 使用 Flow 返回查询结果,数据变化时会自动通知 UI 更新
  • 用枚举类型(PacketType、SourceType)比直接存字符串更安全
  • 数据库实例用单例模式,整个 App 共享一个连接

4.3 CXR 消息协议设计

手机和眼镜之间的通信需要定义一套协议,不然双方不知道怎么解析对方发来的消息。

1. 消息格式定义

/**
 * CXR 基础消息格式
 */
data class CxrBaseMessage(
    val type: String,           // 消息类型
    val data: String,           // 数据 JSON 字符串
    val timestamp: Long = System.currentTimeMillis()
)

/**
 * 红包通知消息
 */
data class RedPacketNotification(
    val action: String,         // "new_packet" 或 "statistics"
    val packet: PacketInfo? = null
)

data class PacketInfo(
    val type: String,           // "receive" 或 "send"
    val amount: Double,
    val source: String,
    val sourceType: String,     // "wechat" 或 "alipay"
    val message: String? = null
)

2. 消息发送封装

class CxrMessageSender(private val cxrManager: CxrManager) {

    /**
     * 发送新红包通知到眼镜
     */
    fun sendNewPacketNotification(
        amount: Double,
        source: String,
        sourceType: SourceType
    ) {
        val notification = RedPacketNotification(
            action = "new_packet",
            packet = PacketInfo(
                type = "receive",
                amount = amount,
                source = source,
                sourceType = sourceType.name.lowercase()
            )
        )

        val baseMessage = CxrBaseMessage(
            type = "red_packet_notification",
            data = Gson().toJson(notification)
        )

        cxrManager.sendMessage(baseMessage)
    }

    /**
     * 发送统计数据到眼镜
     */
    fun sendStatistics(
        totalReceived: Double,
        totalSent: Double,
        netAmount: Double
    ) {
        // 眼镜端显示统计信息
        val content = """
            红包统计
            收入: ¥${totalReceived.toInt()}
            支出: ¥${totalSent.toInt()}
            净收入: ¥${netAmount.toInt()}
        """.trimIndent()

        val baseMessage = CxrBaseMessage(
            type = "statistics_display",
            data = content
        )

        cxrManager.sendMessage(baseMessage)
    }
}

注意点:

  • 消息用 JSON 序列化后再转成 ByteArray 发送
  • 时间戳字段可以用来做消息去重和顺序校验
  • 协议设计要考虑扩展性,方便后续加新字段

4.4 眼镜端显示处理

消息发送到眼镜后,眼镜端需要解析并显示。这部分用的是 CXR-S SDK(虽然本文主要讲移动端,但提一下眼镜端的处理逻辑会更完整)。

1. 眼镜端消息监听

// 眼镜端示例代码(伪代码)
cxrService.onMessage((data) => {
    const message = JSON.parse(data);

    switch (message.type) {
        case 'red_packet_notification':
            handleRedPacketNotification(message.data);
            break;
        case 'statistics_display':
            showStatistics(message.data);
            break;
    }
});

function handleRedPacketNotification(data) {
    const packet = JSON.parse(data);

    // 显示红包提醒卡片
    showNotification({
        title: '🧧 新红包',
        content: `来自 ${packet.source}${packet.amount} 元红包`,
        duration: 5000,
        position: 'top'
    });
}

2. 显示卡片布局示例

<!-- 眼镜端显示卡片结构 -->
<div class="red-packet-card">
    <div class="icon">🧧</div>
    <div class="info">
        <div class="amount">¥200.00</div>
        <div class="source">来自:妈妈</div>
    </div>
    <div class="actions">
        <button>语音回复</button>
        <button>查看详情</button>
    </div>
</div>

注意点:

  • 眼镜端屏幕小,信息要精简,只显示核心内容
  • 提醒卡片展示时间不宜过长,5秒左右自动消失
  • 按钮要足够大,方便手势或触摸板操作

五、CXR SDK 集成:手机与眼镜通信

5.1 连接 Rokid 眼镜

CXR-M SDK 提供了蓝牙设备扫描和连接功能。Rokid 设备有一个特定的服务 UUID,可以通过这个 UUID 来过滤设备。

class CxrManager(private val context: Context) {

    private val cxrClient = CXRClientM.getInstance()
    private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
    val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()

    enum class ConnectionState {
        DISCONNECTED, CONNECTING, CONNECTED, ERROR
    }

    private val ROKID_SERVICE_UUID = UUID.fromString("00009100-0000-1000-8000-00805f9b34fb")

    init {
        initCxrClient()
    }

    private fun initCxrClient() {
        cxrClient.setConnectionStateListener(object : ConnectionStateListener {
            override fun onConnectionStateChanged(state: Int, deviceAddress: String?) {
                when (state) {
                    ConnectionStateListener.STATE_CONNECTED -> {
                        _connectionState.value = ConnectionState.CONNECTED
                        Log.d(TAG, "已连接到眼镜设备")
                    }
                    ConnectionStateListener.STATE_DISCONNECTED -> {
                        _connectionState.value = ConnectionState.DISCONNECTED
                        Log.d(TAG, "设备已断开连接")
                    }
                    ConnectionStateListener.STATE_CONNECTING -> {
                        _connectionState.value = ConnectionState.CONNECTING
                    }
                }
            }
        }, true)
    }

    fun startBluetoothScan(callback: (BluetoothDevice) -> Unit) {
        val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
        val bleScanner = bluetoothAdapter?.bluetoothLeScanner ?: return

        val scanCallback = object : ScanCallback() {
            override fun onScanResult(callbackType: Int, result: ScanResult) {
                val device = result.device
                val uuids = result.scanRecord?.serviceUuids

                // 检查是否是 Rokid 设备
                val isRokidDevice = uuids?.any {
                    it.uuid == ROKID_SERVICE_UUID
                } == true

                if (isRokidDevice) {
                    bleScanner.stopScan(this)
                    callback(device)
                }
            }
        }

        bleScanner.startScan(scanCallback)

        // 10秒后自动停止扫描
        Handler(Looper.getMainLooper()).postDelayed({
            bleScanner.stopScan(scanCallback)
        }, 10000)
    }
}

5.2 发送消息到眼镜

当检测到新红包时,我们需要通过 CXR-M SDK 将消息发送到眼镜端显示。

fun sendRedPacketNotification(
    action: PacketAction,
    packetInfo: PacketInfo? = null,
    statistics: StatisticsInfo? = null
) {
    val redPacketMessage = RedPacketMessage(
        action = action.value,
        packet = packetInfo,
        statistics = statistics
    )

    val baseMessage = CxrBaseMessage(
        type = "red_packet_notification",
        data = Gson().toJson(redPacketMessage),
        timestamp = System.currentTimeMillis()
    )

    if (_connectionState.value == ConnectionState.CONNECTED) {
        val status = cxrClient.sendData(baseMessage.toJson().toByteArray())
        if (status == ValueUtil.CxrStatus.REQUEST_SUCCEED) {
            Log.d(TAG, "消息发送成功")
        }
    }
}

整个消息流程如下:

👤 微信发红包🔔 无障碍服务💾 数据库📱 CXR-M🥽 Rokid眼镜收到红包消息1解析金额和发送者2保存红包记录3发送通知消息4通过蓝牙传输5显示红包提醒6👤 微信发红包🔔 无障碍服务💾 数据库📱 CXR-M🥽 Rokid眼镜

图2:红包通知时序图 - sequenceDiagram - 展示从收到红包到眼镜显示的完整流程

注意点:

  • CXR-M SDK 的 sendData 方法只接受 ByteArray,需要将对象序列化成 JSON 再转字节
  • 连接状态要实时监听,眼镜可能在运行过程中断开连接
  • 消息发送建议加上重试机制,蓝牙传输偶尔会丢包

六、语音交互实现

6.1 语音识别

语音这块我直接用的 Android 自带的 SpeechRecognizer,没啥花里胡哨的。支持的功能也很简单——说话查询"今年收了多少"这种。

class VoiceRecognizer(private val context: Context) {

    private val speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context)
    private val _recognitionState = MutableStateFlow(RecognitionState.IDLE)
    val recognitionState: StateFlow<RecognitionState> = _recognitionState.asStateFlow()

    private val recognitionListener = object : RecognitionListener {
        override fun onReadyForSpeech(params: Bundle?) {
            _recognitionState.value = RecognitionState.LISTENING
        }

        override fun onResults(results: Bundle?) {
            val matches = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
            if (!matches.isNullOrEmpty()) {
                val text = matches[0]
                onResultCallback?.invoke(text)
            }
            _recognitionState.value = RecognitionState.IDLE
        }

        override fun onError(error: Int) {
            val errorMessage = when (error) {
                SpeechRecognizer.ERROR_SPEECH_TIMEOUT -> "说话超时"
                SpeechRecognizer.ERROR_NO_MATCH -> "未识别到语音"
                else -> "识别错误"
            }
            _recognitionState.value = RecognitionState.ERROR(errorMessage)
        }

        // ... 其他回调方法
    }

    fun startListening(
        onResult: (String) -> Unit,
        onError: (String) -> Unit
    ) {
        this.onResultCallback = onResult
        this.onErrorCallback = onError

        val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
            putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
            putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.SIMPLIFIED_CHINESE)
            putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1)
        }

        speechRecognizer.startListening(intent)
    }
}

6.2 语音命令处理

命令匹配用的正则表达式,方案土是土了点,但胜在简单有效。

class VoiceCommandProcessor(
    private val repository: RedPacketRepository,
    private val onResult: (String) -> Unit
) {

    companion object {
        private val QUERY_RECEIVE_PATTERN = Pattern.compile(".*(今年|总共).*收了.*多少.*")
        private val QUERY_SEND_PATTERN = Pattern.compile(".*(今年|总共).*发了.*多少.*")
        private val QUERY_ALL_PATTERN = Pattern.compile(".*(今年|总共).*红包.*统计.*")
    }

    fun processCommand(text: String) {
        when {
            QUERY_RECEIVE_PATTERN.matcher(text).matches() -> queryReceivedAmount()
            QUERY_SEND_PATTERN.matcher(text).matches() -> querySentAmount()
            QUERY_ALL_PATTERN.matcher(text).matches() -> queryStatistics()
            else -> onError("抱歉,我没有理解您的指令")
        }
    }

    private fun queryReceivedAmount() {
        CoroutineScope(Dispatchers.IO).launch {
            val total = repository.redPacketDao().getTotalReceived() ?: 0.0
            withContext(Dispatchers.Main) {
                onResult("您今年一共收到了 ${total.toInt()} 元红包")
            }
        }
    }
}

支持的语音指令:

指令示例 意图
“今年收了多少” 查询收入
“今年发了多少” 查询支出
“红包统计” 查询统计

七、踩坑记录

整个做下来坑挺多的,挑几个印象深的说说:

问题 解决方案
无障碍服务不稳定 添加服务状态监测,服务被杀掉时自动重启
蓝牙连接失败率高 实现指数退避重连策略,间隔时间逐步增加
语音识别准确率低 优化正则匹配,添加模糊匹配和同义词支持
眼镜显示延迟 压缩消息体大小,只传输必要数据
35%25%20%15%5%图3:开发时间分配 - pie - 展示各模块开发耗时占比无障碍服务 [35]CXR SDK集成 [25]语音交互 [20]UI开发 [15]测试调试 [5]

八、预期效果与总结

8.1 预期效果

按照设计,完成后的应用实现效果:

// 场景1:收到红包时的处理流程
fun onRedPacketReceived(amount: Double, source: String) {
    // 1. 保存到数据库
    repository.addRedPacket(
        type = PacketType.RECEIVE,
        amount = amount,
        source = source
    )

    // 2. 发送到眼镜显示
    cxrManager.sendNotification(
        title = "收到红包",
        content = "来自 $source 的 $amount 元红包"
    )

    // 3. 可选:语音播报
    if (settings.voiceEnabled) {
        tts.speak("$source 给你发了 $amount 元红包")
    }
}

// 场景2:语音查询统计
fun onVoiceQuery(command: String) {
    when {
        command.contains("收了") -> {
            val total = repository.getTotalReceived()
            respond("您今年一共收到了 ${total.toInt()} 元红包")
        }
        command.contains("发了") -> {
            val total = repository.getTotalSent()
            respond("您今年一共发出了 ${total.toInt()} 元红包")
        }
    }
}

8.2 开发总结

这个项目的核心难点其实不在技术,而在细节处理上:

  1. CXR SDK 的集成——文档有点简略,但多试几次就能摸清门路
  2. 无障碍服务的稳定性——需要处理各种边界情况,服务被杀后要能自动重启
  3. 语音识别的准确率——正则匹配要做好,不然容易误判

后面还有一些想做的功能,等有空了再补:

改进方向 说明
智能分类 自动识别红包类型(拜年/生日/商务)
云端同步 多设备数据同步,换手机不丢数据
情感分析 分析红包留言的情感倾向
自动回复 根据发送者关系自动选择回复话术


🌟 嗨,我是Xxtaoaooo!
⚙️ 【点赞】让更多同行看见深度干货
🚀 【关注】持续获取行业前沿技术与经验
🧩 【评论】分享你的实战经验或技术困惑
作为一名技术实践者,我始终相信:
每一次技术探讨都是认知升级的契机,期待在评论区与你碰撞灵感火花🔥

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。