基于 Rokid AI Glasses 的春节红包助手:从零到一的技术实践
人们眼中的天才之所以卓越非凡,并非天资超人一等而是付出了持续不断的努力。1万小时的锤炼是任何人从平凡变成超凡的必要条件。———— 马尔科姆·格拉德威尔


🌟 Hello,我是Xxtaoaooo!
🌈 “代码是逻辑的诗篇,架构是思想的交响”
摘要
在移动支付深度融入日常生活的今天,微信红包和支付宝红包已成为春节等节日中传递祝福的重要方式。然而,消息刷屏导致的红包漏看、跨应用的红包统计不便、金额记录混乱等问题,依然是许多用户的痛点
Rokid CXR-M SDK 为开发者提供了连接 Android 手机与 Rokid AI Glasses 的完整能力栈,涵盖蓝牙设备扫描连接、跨设备消息传输、状态同步等核心功能。本文围绕"红包智能管理"这一贴近生活的实用场景,设计并实现一套名为「春节红包助手」的 AR 辅助系统。
该系统通过 Android 无障碍服务(AccessibilityService)监听微信/支付宝的红包消息,自动提取金额与发送者信息并存储至本地数据库,同时通过 CXR-M SDK 将实时通知推送至眼镜端显示,支持语音查询红包统计数据。整个方案严格遵循 SDK 接口规范,在设备连接、消息监听、数据解析、语音交互等环节进行应用开发实践,旨在为开发者提供一个可复现、可落地、贴近生活的技术范本,展示Rokid AR 生态在日常场景中的应用潜力。
一、为什么做这个项目?
春节是中国人最重要的传统节日,而"红包"则是春节最具代表性的文化符号之一。记得去年春节,家族群里红包满天飞,但我总有几个痛点:
- 消息容易漏看——群里消息刷得快,红包往往被淹没
- 金额记不清——发了多少、收了多少,最后算账要翻半天记录
- 跨应用管理麻烦——微信、支付宝都有红包,统计起来很费劲
作为一个技术人,我就想:能不能用 AR 眼镜来解决这个问题?戴上眼镜,红包消息直接显示在眼前,还能自动记账、语音查询,岂不美哉?
说干就干。网上了解看了看 Rokid AR 眼镜,一直想折腾点有意思的东西,这个场景挺合适的,于是就动手了。
二、技术选型:为什么选 Rokid?
其实一开始我也看过几家 AR 眼镜的 SDK,最后选 Rokid 主要是因为:
- 文档还算能看——虽然有些地方写得有点简略,但照着做基本能跑起来
- 论坛里有人踩过坑——遇到问题去开发者论坛里面找一找,大概率能找到答案
- 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, "消息发送成功")
}
}
}
整个消息流程如下:
图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()} 元红包")
}
}
}
}
支持的语音指令:
| 指令示例 | 意图 |
|---|---|
| “今年收了多少” | 查询收入 |
| “今年发了多少” | 查询支出 |
| “红包统计” | 查询统计 |
七、踩坑记录
整个做下来坑挺多的,挑几个印象深的说说:
| 问题 | 解决方案 |
|---|---|
| 无障碍服务不稳定 | 添加服务状态监测,服务被杀掉时自动重启 |
| 蓝牙连接失败率高 | 实现指数退避重连策略,间隔时间逐步增加 |
| 语音识别准确率低 | 优化正则匹配,添加模糊匹配和同义词支持 |
| 眼镜显示延迟 | 压缩消息体大小,只传输必要数据 |
八、预期效果与总结
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 开发总结
这个项目的核心难点其实不在技术,而在细节处理上:
- CXR SDK 的集成——文档有点简略,但多试几次就能摸清门路
- 无障碍服务的稳定性——需要处理各种边界情况,服务被杀后要能自动重启
- 语音识别的准确率——正则匹配要做好,不然容易误判
后面还有一些想做的功能,等有空了再补:
| 改进方向 | 说明 |
|---|---|
| 智能分类 | 自动识别红包类型(拜年/生日/商务) |
| 云端同步 | 多设备数据同步,换手机不丢数据 |
| 情感分析 | 分析红包留言的情感倾向 |
| 自动回复 | 根据发送者关系自动选择回复话术 |

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