从社死边缘拯救我:用 AR 眼镜打造“亲戚称呼助手“

从社死边缘拯救我:用 AR 眼镜打造“亲戚称呼助手“

从社死边缘拯救我:用 AR 眼镜打造"亲戚称呼助手

本文应用基于Rokid灵珠智能体/CXR SDK开发,开发指南https://forum.rokid.com/index

一个真实的新年灾难

大年初二,我跟着新婚妻子回娘家。
刚进门,七大姑八大姨就围了上来。一位头发花白的阿姨笑盈盈地递过来一个红包,我脑子里嗡的一声——这到底是妻子的哪位亲戚?大姨?小姨?还是什么远房表姑?
“小张啊,还认识我不?”
我支支吾吾半天,最后还是妻子打了圆场:“这是大姨,小时候还抱过你呢!”
那一刻,我看到了大姨眼里的失望。这种社死现场,相信很多人都经历过:春节期间,走亲访友是必修课,但那些一年见一次的亲戚,名字和称呼根本记不住。尤其是刚结婚的新人、不常回家的打工人,简直是"称呼灾难"高发人群。
回家后,我下定决心:明年春节,我绝不能再叫错人。

在这里插入图片描述

思路:为什么是 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 的蓝牙连接是异步的,所有结果都通过回调返回。不要试图同步等待连接结果,会导致死锁。

发送数据到眼镜

数据发送是整个应用的关键功能。这里有一个必须注意的顺序:

  1. 先打开提词器场景
  2. 再发送文本数据
fun sendTextToGlasses(text: String, callback: SendCallback? = null): Boolean {if(!isBluetoothConnected){ callback?.onFailed("眼镜未连接")returnfalse} // 关键:必须先打开提词器场景! 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)returnfalse 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())returntrue ActivityCompat.requestPermissions(this, notGranted.toTypedArray(), REQUEST_PERMISSIONS)returnfalse}

坑二:提词器场景必须先打开

这个问题让我调试了整整一下午。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 开发者论坛

Read more

机器人标准DH(SDH)与改进DH(MDH)

机器人标准DH(SDH)与改进DH(MDH)

首先说一下为什么要写这一篇博客,就是为了提醒大家要明确区分标准DH和改进DH。很多机器人初学者只知道用DH法建立串联机器人连杆坐标系,然后在看书或者使用DH的时候很糊涂的就模糊了这标准DH和改进DH的区别,最大的坑就是:一些比较老的机器人学教科书用的是标准DH,而现在比较新的机器人书或者说我们大部分用的都是改进DH,这就导致老的教科书里面的一些公式推导和新的网上找的代码不一致,就会比较麻烦。 一:改进DH法 建立连杆坐标系: 使用改进D-H参数,将 坐标系定义在i 连杆的前端关节: 二:标准DH与改进DH法的区别 我们知道一个连杆有两端,一端离基座近,一端离基座远。简单的来说,标准DH将坐标系i建立在连杆i离基座近的一端,改进DH建立在离基座远的一端。 2.1 机器人连杆与关节的标号 先标号,再建系。 连杆编号:基座为杆0,从基座往后依次定义为杆1,杆2,…,杆i; 关节编号:杆i离基座近的一端(近端)的关节为关节i,远的一端(远端)为关节i+1。 为便于理解,这里我把连杆的近端用绿色表示,远端用橙色表示,且远端驱动近端转动。大家只要记住一句话,连杆近端关节

Spatial Joy 2025 全球 AR&AI 赛事:开发者要的资源、玩法、避坑攻略都在这

Spatial Joy 2025 全球 AR&AI 赛事:开发者要的资源、玩法、避坑攻略都在这

《Spatial Joy 2025 全球 AR&AI 赛事:开发者要的资源、玩法、避坑攻略都在这》 Spatial Joy 2025 Rokid乐奇 全球 AR&AI 开发大赛 值不值得参加?不少参加过连续两届 Rokid乐奇 赛事的老兵,纷纷表示非常值得参加。 先说最实在的——奖金。 AR赛道分为应用和游戏两个赛道,金奖各20万人民币,而且是现金!交完税全是你自己的!这还不够,AR赛道总共设了27个奖项,据我打听到的往年数据,能正常跑进初赛的作品大概就60-70个,这意味着获奖比例相当高。 20万就封顶了吗?远远没有!亚马孙科技给使用Kiro并获奖的开发者,在原奖金基础上再加20%现金奖励! AI赛道同样设置了27个奖项,奖金从1万到5万不等,主要以智能体开发为主,支持市面上所有智能体平台的适配。也就是说,你之前做的智能体微调一下就能参赛! 更重要的是,现在正是智能眼镜行业爆发前夜。据我观察,

组建龙虾团队——OpenClaw多机器人构建

组建龙虾团队——OpenClaw多机器人构建

成功搭建了OpenClaw,也成功建立的自己的每日服务,这时候发现,似乎不太敢在当前的机器人中让他做别的事情,生怕会话太多会让他出现遗忘。(尽管我们配置了QMD记忆增强,但毋庸置疑任何技术都是有上限的)。 换做同样的情况,比如在DeepSeek或者豆包之类的对话窗口,我们会习惯性地新建一个对话。那么我们是否可以新建一个机器人,或者多个机器人,让他们各司其职,各尽所能,形成一个相互配合的团队呢~开干吧,没什么不可能的!! 🦞新建一个机器人 来到飞书开发者后台,新创建一个应用,在这里我们以短视频剪辑脚本应用为例。 创建之后,由于我们的openclaw绑定的是之前的飞书渠道,并没有链接到这个应用的APP ID,所以暂时不做其他操作,只需要记录一下他的APP ID和APP Secret。 🦞配置OpenClaw 如果还是按照claw的命令行安装,每一步都有些让人担心害怕,毕竟我们先前已经配置过一次了,接下来的操作,需要小心是否会把以前的配置给覆盖掉。 为了避免这样的不确定性,我们直接去操作他的配置文件 在WSL2终端中进入openclaw目录 cd .openclaw

FPGA基础知识(十五):Xilinx Clocking Wizard IP核完全指南--从基础到高级应用

FPGA基础知识(十五):Xilinx Clocking Wizard IP核完全指南--从基础到高级应用

《FPGA基础知识》系列导航                本专栏专为FPGA新手打造的Xilinx平台入门指南。旨在手把手带你走通从代码、仿真、约束到生成比特流并烧录的全过程。        本篇是该系列的第十五篇内容        上一篇:FPGA基础知识(十四):FIFO工作原理与基础概念-ZEEKLOG博客        下一篇:FPGA基础知识(十六):Xilinx Block Memory IP核完全指南(1)--核心定位与基础配置-ZEEKLOG博客       在FPGA设计中,时钟管理是整个系统稳定运行的基石。Xilinx的Clocking Wizard IP核作为时钟管理的核心工具,能够极大地简化复杂的时钟设计。本文将带你从基础使用到高级应用,全面掌握这个强大的工具。 一、Clocking Wizard是什么?        Clocking Wizard是Xilinx Vivado设计套件中的一个IP核,用于自动化和简化FPGA中的时钟管理。它提供了一个图形化界面来配置MMCM(混合模式时钟管理器)和PLL(锁相环),让开发者无需深入理解底层复杂的