跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
KotlinAI大前端java

基于 AR 眼镜的亲戚称呼助手开发实战

综述由AI生成介绍如何使用 Rokid AR 眼镜及 CXR-M SDK 开发一款“亲戚称呼助手”应用。通过 Android 端管理亲戚数据(姓名、称呼、关系、话术),利用蓝牙连接 AR 眼镜,将信息以提词器形式同步至眼镜端显示。解决了春节期间难以识别亲戚称呼的痛点。核心涉及 SDK 集成、蓝牙权限处理、场景控制及 TTS 语音播报。

怪力乱神发布于 2026/4/5更新于 2026/5/2130 浏览
基于 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 的蓝牙连接是异步的,所有结果都通过回调返回。不要试图同步等待连接结果,会导致死锁。

发送数据到眼镜

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

  1. 先打开提词器场景
  2. 再发送文本数据
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 开发者论坛

目录

  1. 基于 AR 眼镜的亲戚称呼助手开发实战
  2. 一个真实的新年灾难
  3. 思路:为什么是 AR 眼镜?
  4. 项目搭建:从零开始集成 SDK
  5. 1. 创建项目并配置 Maven 仓库
  6. 2. 添加依赖项
  7. 3. 权限声明
  8. 数据模型:如何存储亲戚信息?
  9. 数据持久化
  10. 核心:眼镜通信模块
  11. SDK 封装思路
  12. 蓝牙连接流程
  13. 发送数据到眼镜
  14. TTS 语音播报
  15. UI 界面:简洁实用优先
  16. 布局结构
  17. 列表项卡片
  18. Activity 核心逻辑
  19. 常见问题与解决方案
  20. 坑一:蓝牙权限动态申请
  21. 坑二:提词器场景必须先打开
  22. 坑三:中文编码
  23. 最终效果
  24. 功能清单
  25. 眼镜端显示效果
  26. 总结与展望
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 常见字符编码方式详解
  • 现代C++异步编程与序列化:yalantinglibs 库解析
  • Flood Fill 算法实战:图像渲染与岛屿问题解析
  • Llama-2-7B 在昇腾 NPU 上的性能测评与部署实践
  • AI 算力提升下的能耗与散热挑战及电源解决方案
  • 微信小程序 AR 开发:5 步实现增强现实应用
  • 2022 CSP-S 提高组策略游戏真题及题解
  • LangChain 实战:工具调用与结构化输出
  • Cgroups 资源控制与 LXC 容器操作流程实战
  • AIGC 助力 Java 编程:智能工具提升效率与创新
  • JVM 垃圾回收核心:对象死亡判断的底层逻辑
  • 深入理解 C++ 异常机制
  • OpenAI 指控 DeepSeek 非法蒸馏,字节发布 Seedance 2.0,Java 26 预览版上线
  • C# 多线程同步机制:Mutex 与 Semaphore 详解
  • Python 基础知识:字符串常用方法汇总
  • 社招三年后端开发核心面试题汇总与解析
  • 微服务链路追踪实战:SkyWalking 与 Zipkin 架构对比及优化
  • LLM 评估指标详解:如何客观对比模型性能
  • Flutter 三方库 mediapipe_core 的鸿蒙化适配指南
  • Axure 制作 AI 自动对话机器人原型教程

相关免费在线工具

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online