从社死边缘拯救我:用 AR 眼镜打造“亲戚称呼助手“
解决方案无非几种:● 记在手机备忘录:掏手机、解锁、搜索,太慢,而且当着亲戚面查手机很不礼貌● 记在小本本上:更尴尬,像是在做作弊小抄● 让家人提醒:每次都要麻烦别人,不靠谱想了很久,我注意到桌上的 Rokid AR 眼镜。对比维度手机AR 眼镜使用隐蔽性众人可见你在查手机只有自己能看到屏幕内容操作便捷度掏出→解锁→搜索→查看抬眼即见,无需动手社交压力明显在看手机,不礼貌自然地瞟一眼,谁也发现不了
从社死边缘拯救我:用 AR 眼镜打造"亲戚称呼助手
一个真实的新年灾难
大年初二,我跟着新婚妻子回娘家。
刚进门,七大姑八大姨就围了上来。一位头发花白的阿姨笑盈盈地递过来一个红包,我脑子里嗡的一声——这到底是妻子的哪位亲戚?大姨?小姨?还是什么远房表姑?
“小张啊,还认识我不?”
我支支吾吾半天,最后还是妻子打了圆场:“这是大姨,小时候还抱过你呢!”
那一刻,我看到了大姨眼里的失望。这种社死现场,相信很多人都经历过:春节期间,走亲访友是必修课,但那些一年见一次的亲戚,名字和称呼根本记不住。尤其是刚结婚的新人、不常回家的打工人,简直是"称呼灾难"高发人群。
回家后,我下定决心:明年春节,我绝不能再叫错人。
思路:为什么是 AR 眼镜?
解决方案无非几种:
● 记在手机备忘录:掏手机、解锁、搜索,太慢,而且当着亲戚面查手机很不礼貌
● 记在小本本上:更尴尬,像是在做作弊小抄
● 让家人提醒:每次都要麻烦别人,不靠谱
想了很久,我注意到桌上的 Rokid AR 眼镜。眼镜有几个天然优势:
| 对比维度 | 手机 | AR 眼镜 |
|---|---|---|
| 使用隐蔽性 | 众人可见你在查手机 | 只有自己能看到屏幕内容 |
| 操作便捷度 | 掏出→解锁→搜索→查看 | 抬眼即见,无需动手 |
| 社交压力 | 明显在看手机,不礼貌 | 自然地瞟一眼,谁也发现不了 |
| 响应速度 | 打开APP需要几秒 | 信息即时显示 |
项目搭建:从零开始集成 SDK
1. 创建项目并配置 Maven 仓库
首先是一个标准的 Android 项目,Kotlin 语言,minSdk 设为 28(SDK 硬性要求)。在 settings.gradle.kts 中添加 Rokid 的 Maven 仓库:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
maven { url = uri("https://maven.rokid.com/repository/maven-public/") }
google()
mavenCentral()
}
}

2. 添加依赖项
在 app/build.gradle.kts 中引入 CXR-M SDK 和必要的 Android 组件:
// app/build.gradle.kts
android {
namespace = "com.rokid.relativehelper"
compileSdk = 34
defaultConfig {
minSdk = 28
targetSdk = 34
}
}
dependencies {
// Rokid CXR-M SDK
implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2")
// Android 组件
implementation("androidx.core:core-ktx:1.12.0")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
}

3. 权限声明
眼镜与手机通过蓝牙通信,需要声明相关权限。Android 12+ 对蓝牙权限做了拆分,需要特别注意:
<!-- AndroidManifest.xml -->
<!-- 蓝牙基础权限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Android 12+ 蓝牙扫描/连接权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 定位权限(部分设备蓝牙扫描需要) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
关键提示:Android 12 以上,BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT 必须在运行时动态申请,仅 Manifest 声明会导致崩溃。
数据模型:如何存储亲戚信息?
先定义一个简洁的数据结构。每个亲戚条目需要:名字、称呼、关系描述、拜年话术,以及可选的拼音和备注:
// model/Relative.kt
data class Relative(
var id: Long = 0,
val name: String, // 亲戚名字,如"王芳"
val title: String, // 称呼,如"大姨"、"叔叔"
val relation: String, // 关系描述,如"妈妈的姐姐"
val greetingTemplate: String, // 拜年话术模板
val phonetic: String? = null, // 名字拼音(可选,防止读错)
val notes: String? = null // 备注(可选)
) {
/**
* 生成眼镜端显示的格式化文本
*/
fun toGlassDisplayText(): String = buildString {
appendLine("👤 $name")
appendLine()
appendLine("称呼:$title")
appendLine("关系:$relation")
appendLine()
appendLine("────── 拜年话术 ──────")
appendLine(greetingTemplate)
}
}

这个 toGlassDisplayText() 方法很关键——它决定了信息在眼镜上的呈现方式。我选择了简洁清晰的格式:
👤 王芳
称呼:大姨
关系:妈妈的姐姐
────── 拜年话术 ──────
大姨新年好!祝您身体健康,万事如意!
这样在眼镜上一眼就能看到关键信息。
数据持久化
亲戚数据需要持久化存储。考虑到数据量不大(通常几十条),我选择了最简单的 SharedPreferences + JSON 序列化方案:
// data/RelativeRepository.kt
object RelativeRepository {
private const val PREFS_NAME = "relative_helper_prefs"
private const val KEY_RELATIVES = "relatives"
private val relatives = mutableListOf<Relative>()
fun init(context: Context) {
loadFromPrefs(context)
if (relatives.isEmpty()) {
loadPresetData() // 首次运行,加载预设数据
}
}
fun searchRelatives(keyword: String): List<Relative> {
if (keyword.isBlank()) return relatives.toList()
val lowerKeyword = keyword.lowercase()
return relatives.filter {
it.name.contains(keyword, ignoreCase = true) ||
it.title.contains(keyword, ignoreCase = true) ||
it.relation.contains(keyword, ignoreCase = true)
}
}
private fun loadPresetData() {
// 内置 20 条常见亲戚数据,涵盖祖辈、父辈、同辈
relatives.addAll(listOf(
Relative(1, "王芳", "大姨", "妈妈的姐姐",
"大姨新年好!祝您身体健康,万事如意!"),
Relative(2, "李明", "叔叔", "爸爸的弟弟",
"叔叔过年好!祝您事业顺利,财源广进!"),
// ... 更多预设
))
}
}
此外,我还实现了一个贴心的小功能:根据称呼自动生成拜年话术。比如输入"爷爷",自动填充"爷爷新年好!祝您身体健康,长命百岁!"
fun generateDefaultGreeting(title: String): String = when (title) {
"爷爷", "外公" -> "${title}新年好!祝您身体健康,长命百岁!"
"奶奶", "外婆" -> "${title}新年好!祝您福如东海,寿比南山!"
"姑姑", "婶婶", "舅妈", "大姨", "小姨" ->
"${title}新年好!祝您青春永驻,越来越年轻!"
"表哥", "堂哥" -> "${title}新年好!祝今年发大财!"
else -> "${title}新年好!祝您新年快乐,万事如意!"
}
核心:眼镜通信模块
这是整个项目最核心的部分——如何让手机和眼镜"对话"?
SDK 封装思路
CXR-M SDK 提供了 CxrApi 类作为通信入口,包含蓝牙连接、场景控制、数据发送等功能。为了方便使用,我将其封装成单例对象 RokidGlassesManager:
// sdk/RokidGlassesManager.kt
object RokidGlassesManager {
private val cxrApi: CxrApi by lazy { CxrApi.getInstance() }
private var connectionCallback: ConnectionCallback? = null
// 连接状态回调接口
interface ConnectionCallback {
fun onConnecting()
fun onConnected()
fun onDisconnected()
fun onFailed(errorMsg: String)
}
// 检查蓝牙是否已连接
val isBluetoothConnected: Boolean
get() = cxrApi.isBluetoothConnected
}

蓝牙连接流程
连接眼镜分为两步:先调用 initBluetooth 获取连接信息,再调用 connectBluetooth 建立实际连接:
fun initBluetoothConnection(context: Context, device: BluetoothDevice) {
connectionCallback?.onConnecting()
// 第一步:初始化蓝牙,获取 UUID 和 MAC 地址
cxrApi.initBluetooth(
context = context,
device = device,
callback = object : BluetoothStatusCallback() {
override fun onConnectionInfo(
socketUuid: String?,
macAddress: String?,
rokidAccount: String?,
glassesType: Int
) {
if (!socketUuid.isNullOrEmpty() && !macAddress.isNullOrEmpty()) {
// 第二步:使用获取的信息建立连接
connectBluetooth(context, socketUuid, macAddress)
} else {
connectionCallback?.onFailed("获取连接信息失败")
}
}
override fun onConnected() {
connectionCallback?.onConnected()
}
override fun onDisconnected() {
connectionCallback?.onDisconnected()
}
override fun onFailed(errorCode: ValueUtil.CxrBluetoothErrorCode?) {
connectionCallback?.onFailed(getBluetoothErrorMessage(errorCode))
}
}
)
}
踩坑提醒:SDK 的蓝牙连接是异步的,所有结果都通过回调返回。不要试图同步等待连接结果,会导致死锁。
发送数据到眼镜
数据发送是整个应用的关键功能。这里有一个必须注意的顺序:
- 先打开提词器场景
- 再发送文本数据
fun sendTextToGlasses(text: String, callback: SendCallback? = null): Boolean {
if (!isBluetoothConnected) {
callback?.onFailed("眼镜未连接")
return false
}
// 关键:必须先打开提词器场景!
openWordTipsScene()
val status = cxrApi.sendStream(
type = ValueUtil.CxrStreamType.WORD_TIPS,
stream = text.toByteArray(Charsets.UTF_8), // 注意编码
fileName = "relative_info.txt",
cb = object : SendStatusCallback() {
override fun onSendSucceed() {
callback?.onSuccess()
}
override fun onSendFailed(errorCode: ValueUtil.CxrSendErrorCode?) {
callback?.onFailed(getSendErrorMessage(errorCode))
}
}
)
return status == ValueUtil.CxrStatus.REQUEST_SUCCEED
}
private fun openWordTipsScene(): Boolean {
val status = cxrApi.controlScene(
sceneType = ValueUtil.CxrSceneType.WORD_TIPS,
openOrClose = true,
otherParams = null
)
return status == ValueUtil.CxrStatus.REQUEST_SUCCEED
}
我第一次调试时,sendStream 返回成功,但眼镜端什么都没显示。排查了半天才发现是场景没打开——这个坑踩得很痛。
TTS 语音播报
除了文字显示,SDK 还支持 TTS(文字转语音)功能,可以在同步信息后播放语音提示:
fun sendTtsFeedback(text: String): Boolean {
if (!isBluetoothConnected) return false
val status = cxrApi.sendTtsContent(text)
if (status == ValueUtil.CxrStatus.REQUEST_SUCCEED) {
// 关键:必须通知 TTS 播放完成
cxrApi.notifyTtsAudioFinished()
}
return status == ValueUtil.CxrStatus.REQUEST_SUCCEED
}
注意:调用 sendTtsContent 后,必须再调用 notifyTtsAudioFinished(),否则 TTS 可能播放不完整。
UI 界面:简洁实用优先
主界面采用经典的列表式布局:
● 顶部:眼镜连接状态指示器 + 连接按钮
● 中间:搜索框 + 亲戚卡片列表
● 右下角:浮动添加按钮
布局结构
<!-- layout/activity_main.xml -->
<androidx.coordinatorlayout.widget.CoordinatorLayout>
<!-- 顶部 Toolbar -->
<com.google.android.material.appbar.AppBarLayout>
<androidx.appcompat.widget.Toolbar
app:title="亲戚称呼助手" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout>
<!-- 眼镜连接状态卡片 -->
<androidx.cardview.widget.CardView>
<LinearLayout>
<View android:id="@+id/connection_indicator" /> <!-- 状态指示灯 -->
<TextView android:id="@+id/tv_connection_status" />
<MaterialButton android:id="@+id/btn_connect" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- 搜索框 -->
<TextInputLayout app:startIconDrawable="@drawable/ic_search">
<TextInputEditText android:id="@+id/et_search" />
</TextInputLayout>
<!-- 亲戚列表 -->
<RecyclerView android:id="@+id/recycler_view" />
</LinearLayout>
<!-- 添加按钮 -->
<FloatingActionButton android:id="@+id/fab_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
列表项卡片
每张卡片展示一个亲戚的关键信息,并提供"同步到眼镜"按钮:
<!-- layout/item_relative.xml -->
<androidx.cardview.widget.CardView>
<LinearLayout android:orientation="vertical">
<!-- 名字(大字加粗) -->
<TextView android:id="@+id/tv_name"
android:textSize="18sp"
android:textStyle="bold" />
<!-- 称呼标签 + 关系描述 -->
<LinearLayout android:orientation="horizontal">
<TextView android:id="@+id/tv_title"
android:background="@drawable/bg_tag" />
<TextView android:id="@+id/tv_relation" />
</LinearLayout>
<!-- 拜年话术预览(最多两行) -->
<TextView android:id="@+id/tv_greeting"
android:maxLines="2"
android:ellipsize="end" />
<!-- 同步按钮 -->
<Button android:id="@+id/btn_sync"
android:text="同步到眼镜" />
</LinearLayout>
</androidx.cardview.widget.CardView>
Activity 核心逻辑
// MainActivity.kt
class MainActivity : AppCompatActivity() {
private val glassesManager = RokidGlassesManager
override fun onCreate(savedInstanceState: Bundle?) {
// 初始化数据
RelativeRepository.init(this)
// 设置眼镜连接回调
glassesManager.setConnectionCallback(object : ConnectionCallback {
override fun onConnected() {
updateConnectionStatus(true)
Snackbar.make(binding.root, "眼镜连接成功", Snackbar.LENGTH_SHORT).show()
}
override fun onDisconnected() {
updateConnectionStatus(false)
}
// ...
})
// 搜索功能
binding.etSearch.addTextChangedListener { text ->
filterRelatives(text?.toString() ?: "")
}
}
private fun syncToGlasses(relative: Relative) {
if (!glassesManager.isBluetoothConnected) {
Snackbar.make(binding.root, "请先连接眼镜", Snackbar.LENGTH_SHORT).show()
return
}
val text = relative.toGlassDisplayText()
glassesManager.sendTextToGlasses(text, object : SendCallback {
override fun onSuccess() {
Snackbar.make(binding.root, "已同步到眼镜", Snackbar.LENGTH_SHORT).show()
// 语音提示
glassesManager.sendTtsFeedback("${relative.title}的信息")
}
override fun onFailed(errorMsg: String) {
Snackbar.make(binding.root, "同步失败: $errorMsg", Snackbar.LENGTH_LONG).show()
}
})
}
}
踩坑实录:那些文档没告诉你的事
坑一:蓝牙权限动态申请
Android 12(API 31)对蓝牙权限做了重大调整,将原来的 BLUETOOTH 和 BLUETOOTH_ADMIN 拆分为更细粒度的 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT。
关键点:这些新权限必须运行时动态申请。仅 Manifest 声明在 Release 版本中会直接崩溃。
private fun checkAndRequestPermissions(): Boolean {
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.ACCESS_FINE_LOCATION
)
} else {
arrayOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
Manifest.permission.ACCESS_FINE_LOCATION
)
}
val notGranted = permissions.filter {
ActivityCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (notGranted.isEmpty()) return true
ActivityCompat.requestPermissions(this, notGranted.toTypedArray(), REQUEST_PERMISSIONS)
return false
}
坑二:提词器场景必须先打开
这个问题让我调试了整整一下午。sendStream 调用返回成功,但眼镜端什么都没显示。
最后发现:必须先调用 controlScene 打开提词器场景,才能发送数据。
// 正确顺序
cxrApi.controlScene(WORD_TIPS, true, null) // 先打开场景
cxrApi.sendStream(WORD_TIPS, data, ...) // 再发送数据
坑三:中文编码
第一次发送中文内容,眼镜上显示一堆乱码。原因是 toByteArray() 默认使用系统编码,在某些设备上可能不是 UTF-8。
// 错误
stream = text.toByteArray()
// 正确
stream = text.toByteArray(Charsets.UTF_8)
最终效果
功能清单
| 功能 | 状态 | 说明 |
|---|---|---|
| 亲戚列表 | ✅ | 支持按名字/称呼/关系搜索 |
| 添加/编辑/删除 | ✅ | 表单输入,自动生成话术 |
| 眼镜连接 | ✅ | 自动发现已配对设备 |
| 眼镜同步 | ✅ | 一键发送称呼+话术 |
| TTS 播报 | ✅ | 语音反馈同步成功 |
| 预设数据 | ✅ | 内置 20 条常见亲戚 |
眼镜端显示效果
当你在手机上点击"同步到眼镜"后,眼镜屏幕上会立即显示:
┌─────────────────────────────┐
│ │
│ 👤 王芳 │
│ │
│ 称呼:大姨 │
│ 关系:妈妈的姐姐 │
│ │
│ ────── 拜年话术 ────── │
│ │
│ 大姨新年好! │
│ 祝您身体健康,万事如意! │
│ │
└─────────────────────────────┘
春节拜年时,当亲戚走过来,你只需要悄悄瞟一眼眼镜,称呼和话术尽收眼底,从容应对,再也不会叫错人了。
总结与展望
这个项目从想法到完成只用了两天时间,代码量不大(约 800 行),但确实解决了一个真实痛点。
技术亮点:
● CXR-M SDK 的正确使用方式(场景控制 + 数据发送的顺序问题)
● Android 12+ 蓝牙权限的正确处理
● 简洁实用的数据模型设计
未来改进方向:
● 加入语音识别,说"这个是谁"自动识别并显示
● 支持拍照识别亲戚(需要人脸识别技术)
● 关系图谱可视化,直观展示家族关系
● 云端数据同步,换手机不丢数据
项目源码:RelativeTitleHelper/
相关资源:
● CXR-M SDK 官方文档
● Rokid 开发者论坛
更多推荐



所有评论(0)