Rokid应用实践:基于AI Glasses 智慧快递站“解放双手”的快件录入归类助手
本文介绍了基于Rokid智能眼镜的快递分拣解决方案。该系统采用"眼镜端采集+手机端协同+云端同步"三层架构,通过CXR-S/M SDK实现设备连接、条码识别和数据处理。眼镜端完成本地条码扫描和语音交互,手机端负责数据校验和云端通信,Wi-Fi P2P保障高速数据传输。系统解决了移动场景连接稳定性、条码识别准确率和多指令并发处理等难点,通过蓝牙/Wi-Fi双模切换、本地+云端双识
·
一、创意缘起:工作堆积如山,科技顺其有序
场景:
下午 2 点的快递站仓库,王师傅蹲在堆积如山的快件中,左手抱着一摞包裹,右手紧握扫码枪对准条码扫描。他需要频繁弯腰将快件放入对应货架格,汗水浸湿后背工装。
当 Rokid AI Glasses 智能眼镜遇见智慧物流
在快递业务量持续增长的今天,快递站工作人员面临着巨大的分拣压力。传统的快件录入需要反复查看面单、手动输入信息、分类摆放,整个过程耗时耗力且容易出错。而 Rokid AI Glasses 的出现,为这一场景带来了新的解决方案。
本文将详细介绍如何利用 Rokid CXR-M(移动端)和 CXR-S(眼镜端)SDK,构建一个解放双手的快件录入归类助手,实现"所见即所得"的智能分拣体验。
二、系统架构设计
架构总览
系统采用 “眼镜端采集 + 手机端协同 + 云端同步” 的三层架构,核心依赖 Rokid SDK 实现设备交互与数据流转:
• 终端层(CXR-S AI眼镜)
作为“感知与输出终端”,负责快件条码识别、语音指令接收、操作指引显示,基于 CXR-S SDK 实现本地 AI 识别与状态监听
• 业务逻辑层(CXR-M移动设备)
通过 CXR-M SDK 实现设备连接管理、数据缓存、云端通信,承接眼镜端采集的数据并同步至管理系统。
• 云端层(数据服务)
提供快件信息校验、归类规则存储、数据统计分析功能,通过 API 与手机端实时交互。
核心技术依赖
- 设备连接:基于 CXR-M SDK 的蓝牙扫描、Wi-Fi P2P 连接能力,保障设备稳定通信。
- 数据采集:借助眼镜端相机接口(CXR-M SDK openGlassCamera)实现条码扫描,语音识别接口接收操作指令。
- 交互展示:通过提词器场景(configWordTipsText)显示快件信息与归类指引,自定义界面场景展示实时数据。
- 数据同步:利用 Wi-Fi P2P 高速传输能力(startSync)实现快件图片、信息的即时同步。
三、关键功能技术实现
(一).眼镜端(CXR-S SDK)集成配置
1.环境准备与依赖导入
配置 Maven 仓库
在项目settings.gradle.kts中添加 Rokid Maven 仓库,确保 SDK 包正常拉取:
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
// 添加Rokid Maven仓库
maven {
url = uri("https://maven.rokid.com/repository/maven-public/")
}
mavenCentral()
}
}
rootProject.name = "ExpressSorting_Glasses"
include(":app")
导入 CXR-S SDK 依赖
在app/build.gradle.kts中添加 SDK 依赖,设置最小 SDK 版本≥28:
android {
namespace = "com.rokid.expresssorting.glasses"
compileSdk = 34
defaultConfig {
applicationId = "com.rokid.expresssorting.glasses"
minSdk = 28 // 必须≥28
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
// 基础依赖
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
// 导入CXR-S SDK
implementation("com.rokid.cxr:cxr-service-bridge:1.0-20250519.061355-45")
// 条码解析库(本地识别)
implementation("com.google.zxing:core:3.5.1")
}
2.眼镜端核心初始化(CXRServiceBridge)
实现 SDK 核心类CXRServiceBridge的初始化,配置连接状态监听与消息订阅,支撑条码识别与指令交互:
import android.app.Application
import com.rokid.cxr.CXRServiceBridge
import com.rokid.cxr.Caps
import android.util.Log
class ExpressSortingApp : Application() {
companion object {
const val TAG = "ExpressSorting_Glasses"
lateinit var cxrBridge: CXRServiceBridge
private set
}
override fun onCreate() {
super.onCreate()
// 1. 初始化CXRServiceBridge(必须在主线程初始化)
cxrBridge = CXRServiceBridge()
// 2. 设置连接状态监听(监听手机端连接)
initStatusListener()
// 3. 订阅手机端指令消息(如条码识别请求、分拣指引更新)
subscribeMobileCommands()
}
/**
* 初始化连接状态监听
*/
private fun initStatusListener() {
cxrBridge.setStatusListener(object : CXRServiceBridge.StatusListener {
override fun onConnected(name: String, type: Int) {
Log.d(TAG, "已连接手机设备:$name,设备类型:${getDeviceTypeDesc(type)}")
// 连接成功后,初始化本地相机参数(为条码扫描做准备)
initLocalCameraParams()
}
override fun onDisconnected() {
Log.d(TAG, "与手机设备断开连接")
// 断开连接后,释放相机资源
releaseCameraResources()
}
override fun onARTCStatus(health: Float, reset: Boolean) {
Log.d(TAG, "ARTC连接健康度:${(health * 100).toInt()}%,是否重置:$reset")
}
})
}
/**
* 订阅手机端指令消息(普通消息订阅模式)
*/
private fun subscribeMobileCommands() {
// 订阅"条码识别请求"指令
val scanCmdSubscribeResult = cxrBridge.subscribe("mobile_cmd_scan_barcode",
object : CXRServiceBridge.MsgCallback {
override fun onReceive(name: String, args: Caps, value: ByteArray?) {
Log.d(TAG, "收到手机端条码识别请求:$name")
// 解析请求参数(如分辨率、压缩质量)
val width = if (args.size() > 0) args.at(0).getInt() else 1920
val height = if (args.size() > 1) args.at(1).getInt() else 1080
val quality = if (args.size() > 2) args.at(2).getInt() else 80
// 执行本地条码扫描
LocalBarcodeScanner.scan(width, height, quality)
}
})
if (scanCmdSubscribeResult == 0) {
Log.d(TAG, "条码识别请求指令订阅成功")
} else {
Log.e(TAG, "条码识别请求指令订阅失败,错误码:$scanCmdSubscribeResult")
}
// 订阅"分拣指引更新"指令
val guideCmdSubscribeResult = cxrBridge.subscribe("mobile_cmd_update_guide",
object : CXRServiceBridge.MsgCallback {
override fun onReceive(name: String, args: Caps, value: ByteArray?) {
Log.d(TAG, "收到手机端分拣指引更新:$name")
// 解析指引信息并显示(提词器场景)
if (args.size() > 0) {
val guideText = args.at(0).getString()
GuideDisplayManager.showGuide(guideText)
}
}
})
if (guideCmdSubscribeResult == 0) {
Log.d(TAG, "分拣指引更新指令订阅成功")
} else {
Log.e(TAG, "分拣指引更新指令订阅失败,错误码:$guideCmdSubscribeResult")
}
}
/**
* 初始化本地相机参数(通过Caps写入配置)
*/
private fun initLocalCameraParams() {
val cameraConfig = Caps()
cameraConfig.write("init_camera_params") // 指令标识
cameraConfig.writeInt32(1920) // 默认宽度
cameraConfig.writeInt32(1080) // 默认高度
cameraConfig.writeInt32(80) // 默认质量
// 发送配置到底层(通过sendMessage接口)
val sendResult = cxrBridge.sendMessage("glasses_cmd_init_camera", cameraConfig)
if (sendResult != 0) {
Log.e(TAG, "相机参数初始化失败,错误码:$sendResult")
}
}
/**
* 释放相机资源
*/
private fun releaseCameraResources() {
val releaseCmd = Caps()
releaseCmd.write("release_camera")
val sendResult = cxrBridge.sendMessage("glasses_cmd_release_camera", releaseCmd)
if (sendResult != 0) {
Log.e(TAG, "相机资源释放失败,错误码:$sendResult")
}
}
/**
* 解析设备类型
*/
private fun getDeviceTypeDesc(type: Int): String {
return when (type) {
CXRServiceBridge.StatusListener.DEVICE_TYPE_ANDROID -> "Android手机"
CXRServiceBridge.StatusListener.DEVICE_TYPE_IOS -> "iPhone"
else -> "未知设备"
}
}
}
3 .眼镜端本地条码识别与指引显示
基于 CXR-S SDK 的Caps数据结构与相机接口,实现本地条码扫描、结果回传与分拣指引显示:
// 本地条码扫描工具类
import com.rokid.cxr.CXRServiceBridge
import com.rokid.cxr.Caps
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.google.zxing.BarcodeFormat
import com.google.zxing.DecodeHintType
import com.google.zxing.MultiFormatReader
import com.google.zxing.Result
import com.google.zxing.common.HybridBinarizer
import com.google.zxing.BinaryBitmap
import com.google.zxing.RGBLuminanceSource
import java.util.EnumMap
import java.io.ByteArrayOutputStream
object LocalBarcodeScanner {
private const val TAG = "LocalBarcodeScanner"
private val cxrBridge = ExpressSortingApp.cxrBridge
/**
* 执行本地条码扫描
* @param width 扫描分辨率宽度
* @param height 扫描分辨率高度
* @param quality 图像压缩质量(0-100)
*/
fun scan(width: Int, height: Int, quality: Int) {
// 1. 调用本地相机接口获取条码图像(对接眼镜端硬件相机)
val barcodeImage = captureBarcodeImage(width, height, quality)
if (barcodeImage == null) {
Log.e(TAG, "相机采集图像失败")
sendScanResult(false, "采集失败", null)
return
}
// 2. 解析条码信息(使用ZXing库)
val decodeResult = decodeBarcode(barcodeImage)
if (decodeResult != null) {
Log.d(TAG, "本地解析条码成功:${decodeResult.text}")
// 3. 回传识别结果到手机端
sendScanResult(true, decodeResult.text, barcodeImage)
} else {
Log.e(TAG, "本地解析条码失败,触发云端解析")
// 4. 本地解析失败,将图像回传手机端发起云端解析
sendScanResult(false, "本地解析失败", barcodeImage)
}
}
/**
* 调用眼镜端相机采集条码图像
*/
private fun captureBarcodeImage(width: Int, height: Int, quality: Int): ByteArray? {
// 实际项目需对接眼镜端相机API,此处模拟采集流程
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.WEBP, quality, outputStream)
return outputStream.toByteArray()
}
/**
* 解析条码信息(ZXing实现)
*/
private fun decodeBarcode(imageData: ByteArray): Result? {
val options = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
options[DecodeHintType.CHARACTER_SET] = "UTF-8"
options[DecodeHintType.POSSIBLE_FORMATS] = listOf(
BarcodeFormat.CODE_128,
BarcodeFormat.CODE_39,
BarcodeFormat.EAN_13,
BarcodeFormat.EAN_8,
BarcodeFormat.UPC_A
)
val reader = MultiFormatReader()
reader.setHints(options)
try {
val bitmap = BitmapFactory.decodeByteArray(imageData, 0, imageData.size)
val source = RGBLuminanceSource(bitmap.width, bitmap.height, getPixels(bitmap))
val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
return reader.decode(binaryBitmap)
} catch (e: Exception) {
Log.e(TAG, "条码解析异常:${e.message}")
return null
}
}
/**
* 回传扫描结果到手机端(使用CXR-S SDK的sendMessage接口)
*/
private fun sendScanResult(success: Boolean, result: String, imageData: ByteArray?) {
val resultCaps = Caps()
resultCaps.write(if (success) "scan_success" else "scan_failed") // 状态标识
resultCaps.write(result) // 结果文本
if (imageData != null) {
resultCaps.write(imageData) // 图像数据(可选)
}
// 发送结果到手机端
val sendResult = cxrBridge.sendMessage("glasses_result_scan", resultCaps, imageData)
if (sendResult != 0) {
Log.e(TAG, "结果回传失败,错误码:$sendResult")
}
}
/**
* 辅助方法:获取Bitmap像素数组
*/
private fun getPixels(bitmap: Bitmap): IntArray {
val width = bitmap.width
val height = bitmap.height
val pixels = IntArray(width * height)
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
return pixels
}
}
// 分拣指引显示管理类
import com.rokid.cxr.Caps
import com.rokid.cxr.client.extend.CxrApi
import com.rokid.cxr.client.extend.utils.ValueUtil
object GuideDisplayManager {
private const val TAG = "GuideDisplayManager"
private val cxrBridge = ExpressSortingApp.cxrBridge
/**
* 在眼镜端提词器显示分拣指引
*/
fun showGuide(guideText: String) {
// 1. 配置提词器样式(通过Caps传递参数)
val configCaps = Caps()
configCaps.write("config_word_tips")
configCaps.writeFloat(18f) // textSize
configCaps.writeFloat(4f) // lineSpace
configCaps.write("normal") // mode
configCaps.writeInt32(100) // startPointX
configCaps.writeInt32(200) // startPointY
configCaps.writeInt32(800) // width
configCaps.writeInt32(400) // height
// 发送配置到提词器场景
val configResult = cxrBridge.sendMessage("glasses_cmd_config_guide", configCaps)
if (configResult != 0) {
Log.e(TAG, "提词器配置失败,错误码:$configResult")
return
}
// 2. 显示指引文本
val textCaps = Caps()
textCaps.write("show_guide_text")
textCaps.write(guideText)
val textResult = cxrBridge.sendMessage("glasses_cmd_show_guide", textCaps)
if (textResult != 0) {
Log.e(TAG, "指引文本显示失败,错误码:$textResult")
}
}
/**
* 语音播报指引(通过TTS接口)
*/
fun speakGuide(guideText: String) {
val ttsCaps = Caps()
ttsCaps.write("tts_guide")
ttsCaps.write(guideText)
val ttsResult = cxrBridge.sendMessage("glasses_cmd_tts", ttsCaps)
if (ttsResult != 0) {
Log.e(TAG, "TTS播报失败,错误码:$ttsResult")
}
}
(二)手机端(CXR-M SDK)集成配置
1.环境准备与依赖导入
配置 Maven 仓库
在settings.gradle.kts中添加 Rokid Maven 仓库:
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
// 添加Rokid Maven仓库
maven { url = uri("https://maven.rokid.com/repository/maven-public/") }
google()
mavenCentral()
}
}
rootProject.name = "ExpressSorting_Mobile"
include(":app")
导入 CXR-M SDK 依赖与权限配置
在app/build.gradle.kts中添加 SDK 依赖,设置minSdk≥28
android {
namespace = "com.rokid.expresssorting.mobile"
compileSdk = 34
defaultConfig {
applicationId = "com.rokid.expresssorting.mobile"
minSdk = 28 // 必须≥28
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
// 基础依赖
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
// 导入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("org.jetbrains.kotlin:kotlin-stdlib:2.1.0")
implementation("com.squareup.okio:okio:2.8.0")
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
// 条码解析库(云端解析备用)
implementation("com.google.zxing:core:3.5.1")
// 网络请求库(对接云端API)
implementation("com.squareup.retrofit2:adapter-rxjava2:2.9.0")
}
2.手机端核心初始化(CxrApi)
实现CxrApi单例初始化,配置蓝牙扫描、设备连接与 Wi-Fi P2P 管理:
import android.app.Application
import android.bluetooth.BluetoothDevice
import android.content.Context
import com.rokid.cxr.client.extend.CxrApi
import com.rokid.cxr.client.extend.callbacks.BluetoothStatusCallback
import com.rokid.cxr.client.extend.callbacks.WifiP2PStatusCallback
import com.rokid.cxr.client.extend.utils.ValueUtil
import android.util.Log
class ExpressSortingMobileApp : Application() {
// 1. 全局变量:存储设备连接信息与状态
companion object {
const val TAG = "ExpressSorting_Mobile" // 日志标签
lateinit var instance: ExpressSortingMobileApp // 应用上下文单例
private set
var savedDevice: BluetoothDevice? = null // 已连接的眼镜设备
var savedUuid: String? = null // 设备UUID(蓝牙连接关键参数)
var savedMac: String? = null // 设备MAC地址(重连关键参数)
var isDeviceConnected = false // 设备连接状态标记
}
override fun onCreate() {
super.onCreate()
instance = this // 初始化应用上下文
// 2. 初始化核心模块:CxrApi、蓝牙扫描、数据同步
initCxrApi() // 初始化CXR-M SDK核心
BluetoothScanHelper.init(this) // 初始化蓝牙扫描工具
DataSyncManager.init(this) // 初始化数据同步管理器
}
/**
* 3. CxrApi单例初始化(SDK核心入口)
*/
private fun initCxrApi() {
// 获取CxrApi单例(SDK全局唯一实例,无需重复创建)
val cxrApi = CxrApi.getInstance()
// 打印SDK版本信息(调试用,确认SDK正常加载)
Log.d(TAG, "CXR-M SDK版本信息:${getSdkVersion()}")
// 后续模块(蓝牙连接、Wi-Fi初始化、消息订阅)将在此处扩展
}
/**
* 辅助方法:获取SDK版本(从CxrApi内部属性解析)
*/
private fun getSdkVersion(): String {
// 版本信息来自CxrApi源码定义(见文档5:CxrApi.txt)
return "版本名:1.0.1,版本号:101,构建时间:2025-08-12 16:01:17"
}
}
3 手机端蓝牙扫描与云端交互工具类
配置蓝牙连接回调(BluetoothStatusCallback),监听眼镜端连接、断开、失败状态。
- 解析眼镜端设备信息(UUID、MAC 地址)并缓存,为后续重连提供参数。
- 实现断开自动重连逻辑,保障移动场景下连接稳定性。
private fun initCxrApi() {
val cxrApi = CxrApi.getInstance()
Log.d(TAG, "CXR-M SDK版本信息:${getSdkVersion()}")
// 4. 配置蓝牙连接回调:监听眼镜端蓝牙状态变化
cxrApi.setBluetoothStatusCallback(object : BluetoothStatusCallback {
/**
* 回调1:获取眼镜端设备信息(连接成功后触发)
* @param socketUuid:蓝牙通信UUID(关键连接参数)
* @param macAddress:设备MAC地址(重连用)
* @param rokidAccount:Rokid账号(可选,用于账号绑定)
* @param glassesType:眼镜类型(0=无屏,1=有屏)
*/
override fun onConnectionInfo(
socketUuid: String?,
macAddress: String?,
rokidAccount: String?,
glassesType: Int
) {
Log.d(TAG, "获取眼镜设备信息:UUID=$socketUuid, MAC=$macAddress, 类型=$glassesType")
// 缓存设备信息(重连时复用,避免重复扫描)
savedUuid = socketUuid
savedMac = macAddress
}
/**
* 回调2:蓝牙连接成功(可触发后续Wi-Fi初始化)
*/
override fun onConnected() {
Log.d(TAG, "眼镜端蓝牙连接成功")
isDeviceConnected = true // 更新连接状态
initWifiP2P() // 连接成功后,初始化Wi-Fi(用于数据同步)
subscribeGlassesResults() // 订阅眼镜端消息(如条码识别结果)
}
/**
* 回调3:蓝牙连接断开(触发自动重连)
*/
override fun onDisconnected() {
Log.d(TAG, "眼镜端蓝牙连接断开")
isDeviceConnected = false // 更新连接状态
// 自动重连:复用缓存的设备信息
savedDevice?.let { device ->
connectToGlasses(this@ExpressSortingMobileApp, device)
}
}
/**
* 回调4:蓝牙连接失败(打印错误原因)
* @param errorCode:错误码(见ValueUtil.CxrBluetoothErrorCode)
* - PARAM_INVALID:参数错误(如UUID为空)
* - BLE_CONNECT_FAILED:BLE连接失败
* - SOCKET_CONNECT_FAILED:Socket连接失败
*/
override fun onFailed(errorCode: ValueUtil.CxrBluetoothErrorCode?) {
Log.e(TAG, "蓝牙连接失败,错误码:${errorCode?.name}")
isDeviceConnected = false
}
})
}
/**
* 辅助方法:主动连接眼镜设备(用于首次连接或重连)
* @param context:应用上下文
* @param device:目标蓝牙设备(从扫描结果获取)
*/
fun connectToGlasses(context: Context, device: BluetoothDevice) {
savedDevice = device // 缓存目标设备
val cxrApi = CxrApi.getInstance()
// 调用CxrApi连接接口:需传入缓存的UUID、MAC地址与回调
val connectResult = cxrApi.connectBluetooth(
context = context,
socketUuid = savedUuid ?: "", // 从onConnectionInfo缓存获取
macAddress = savedMac ?: "", // 从onConnectionInfo缓存获取
callback = cxrApi.getBluetoothStatusCallback() as BluetoothStatusCallback // 复用已配置的回调
)
// 检查连接请求是否发起成功(非实际连接结果,仅请求状态)
if (connectResult != ValueUtil.CxrStatus.REQUEST_SUCCEED) {
Log.e(TAG, "发起蓝牙连接请求失败,结果:${connectResult?.name}")
}
}
4Wi-Fi P2P 初始化(用于高速数据同步)
- 蓝牙连接成功后,自动初始化 Wi-Fi P2P 连接(用于传输大文件,如条码图像、快件信息)。
- 监听 Wi-Fi 连接状态,连接成功后触发未同步数据同步;失败则打印错误原因。
- 基于 SDK 接口initWifiP2P与isWifiP2PConnected实现状态管理。
/**
* 5. 初始化Wi-Fi P2P(蓝牙连接成功后触发)
* 作用:高速传输大文件(如条码图像、批量快件数据),弥补蓝牙带宽不足
*/
private fun initWifiP2P() {
val cxrApi = CxrApi.getInstance()
// 调用CxrApi初始化Wi-Fi P2P,传入状态回调
val initResult = cxrApi.initWifiP2P(object : WifiP2PStatusCallback {
/**
* Wi-Fi P2P连接成功(触发数据同步)
*/
override fun onConnected() {
Log.d(TAG, "Wi-Fi P2P连接成功,可开始同步数据")
// 触发未同步数据同步(如之前缓存的条码图像)
DataSyncManager.syncUnsyncedData()
}
/**
* Wi-Fi P2P连接断开
*/
override fun onDisconnected() {
Log.d(TAG, "Wi-Fi P2P连接断开,暂停数据同步")
}
/**
* Wi-Fi P2P连接失败(打印错误原因)
* @param errorCode:错误码(见ValueUtil.CxrWifiErrorCode)
* - WIFI_DISABLED:手机Wi-Fi未开启
* - WIFI_CONNECT_FAILED:P2P连接失败
* - UNKNOWN:未知错误
*/
override fun onFailed(errorCode: ValueUtil.CxrWifiErrorCode?) {
Log.e(TAG, "Wi-Fi P2P连接失败,错误码:${errorCode?.name}")
}
})
// 检查Wi-Fi初始化请求是否发起成功
if (initResult != ValueUtil.CxrStatus.REQUEST_SUCCEED) {
Log.e(TAG, "Wi-Fi P2P初始化请求失败,结果:${initResult?.name}")
}
}
5订阅眼镜端消息(接收条码识别结果)
- 订阅眼镜端发送的 “条码识别结果” 消息(使用可回复订阅模式MsgReplyCallback)。
- 解析眼镜端返回的识别结果(成功 / 失败、条码文本、图像数据),触发后续业务逻辑(如快件信息校验、分拣指引)。
- 回复眼镜端 “结果已收到”,完成消息闭环。
/**
* 6. 订阅眼镜端消息:接收条码识别结果(可回复模式)
* 消息名:glasses_result_scan(需与眼镜端发送的消息名一致,见3.1.3)
*/
private fun subscribeGlassesResults() {
val cxrApi = CxrApi.getInstance()
// 调用CxrApi订阅接口:传入消息名与可回复回调
val subscribeResult = cxrApi.subscribe(
name = "glasses_result_scan", // 消息名(与眼镜端约定)
cb = object : CxrApi.MsgReplyCallback {
/**
* 接收眼镜端消息回调
* @param name:消息名(验证是否为目标消息)
* @param args:结构化参数(Caps格式,存储识别状态、条码文本)
* @param value:二进制数据(可选,如条码图像)
* @param reply:回复对象(用于向眼镜端发送“结果已收到”)
*/
override fun onReceive(
name: String,
args: com.rokid.cxr.Caps,
value: ByteArray?,
reply: CxrApi.Reply?
) {
Log.d(TAG, "收到眼镜端条码识别结果消息:$name")
// 校验参数合法性(args不能为空,否则无法解析结果)
if (args.size() == 0) {
Log.e(TAG, "识别结果参数为空,无法解析")
return
}
// 解析识别结果(从Caps中按顺序读取参数)
val resultStatus = args.at(0).getString() // 第1个参数:状态(scan_success/scan_failed)
val resultText = args.at(1).getString() // 第2个参数:条码文本(成功时为单号,失败时为原因)
val imageData = if (args.size() > 2) args.at(2).getBinary().data else null // 第3个参数:条码图像(可选)
// 分支1:本地识别成功→直接处理快件信息
if (resultStatus == "scan_success") {
ExpressManager.processExpressInfo(resultText, imageData)
}
// 分支2:本地识别失败→触发云端识别
else {
CloudBarcodeDecoder.decode(imageData) { cloudResult ->
if (cloudResult != null) {
ExpressManager.processExpressInfo(cloudResult, imageData)
} else {
Log.e(TAG, "本地+云端解析均失败,需人工处理")
}
}
}
// 回复眼镜端:告知“结果已收到”(完成消息闭环)
val replyCaps = com.rokid.cxr.Caps()
replyCaps.write("result_received") // 回复内容(简单状态标识)
reply?.end(replyCaps) // 发送回复
}
}
)
// 检查订阅请求是否成功
if (subscribeResult != 0) {
Log.e(TAG, "订阅条码识别结果消息失败,错误码:$subscribeResult")
// 错误码说明:-1=参数错误(如消息名为空),-2=重复订阅
}
}
(三)关键功能技术说明
1.设备连接与双模切换
蓝牙保活与重连
- 保活机制:通过CXR-M SDK的isBluetoothConnected定期检查连接状态,闲置时维持低功耗连接,避免频繁断连。
- 重连逻辑:断开后 3 秒内自动调用connectBluetooth复用savedUuid与savedMac重连,3 次失败后触发语音提醒工作人员。
Wi-Fi P2P 自动触发
- 触发条件:当检测到需同步文件(如条码图像、快件信息)时,自动调用initWifiP2P初始化 Wi-Fi 连接,同步完成后 30 秒自动释放资源。
- 状态监听:通过isWifiP2PConnected判断 Wi-Fi 状态,未连接时缓存数据,连接后自动触发同步。
2. 快件信息采集与识别
条码扫描实现
利用眼镜端相机接口实现条码快速识别,配合 AI 优化识别算法:
- 相机配置:通过 CXR-M SDK setPhotoParams设置扫描分辨率(推荐 1920x1080),调用openGlassCamera打开眼镜端相机,takeGlassPhoto拍摄条码图像。
- 本地识别:眼镜端通过 CXR-S SDK 的图像识别能力解析条码信息,若本地识别失败,将图像通过 Wi-Fi 同步至手机端进行云端识别。
- 信息校验:手机端接收条码信息后,调用云端 API 校验快件单号合法性、收件人信息完整性,通过提词器场景(setWordTipsText)在眼镜端显示校验结果。
语音指令交互
基于 Rokid 语音识别能力,支持以下核心指令:
- 主动触发:“扫描快件”“确认归类”“查询库存” 等操作指令。
- 被动反馈:眼镜端通过 TTS 接口(sendTTSContent)播报 “扫描成功”“请归类至 A 区 3 号架” 等反馈信息。
3. 智能归类与指引
归类规则引擎
- 云端配置:快递站根据区域、收件人地址、快件类型预设归类规则(如 “同城件→A 区”“大件→B 区”)。
- 实时匹配:手机端接收快件信息后,调用云端 API 获取归类结果,通过sendStream接口将指引信息推送至眼镜端。
- 视觉指引:在眼镜端自定义界面(openCustomView)显示归类区域示意图,配合语音播报完成精准分拣。
异常处理机制
- 条码识别失败:语音提示 “请调整角度重新扫描”,并在提词器显示操作指引。
- 归类规则不存在:自动标记为 “待人工处理”,同步至管理系统并提醒工作人员。
- 网络中断:数据缓存至手机端(sendStream临时存储),网络恢复后自动同步(startSync)。
4. 数据实时同步与管理
- 本地缓存:手机端通过 CXR-M SDK 的sendStream接口缓存快件信息与图像,保障离线状态下的操作连续性。
- 云端同步:Wi-Fi 连接状态下,调用startSync接口将缓存数据同步至云端,支持单个文件同步(syncSingleFile)与批量同步。
- 状态监听:通过MediaFilesUpdateListener监听眼镜端媒体文件更新,确保扫描图像无遗漏同步。
四、核心难点与解决方案
难点 1:移动场景下设备连接稳定性
问题:快递站空间大、人员移动频繁,蓝牙连接易受干扰,Wi-Fi 切换需无缝衔接。解决方案:
- 实现蓝牙与 Wi-Fi 双模自动切换:蓝牙负责日常指令传输,Wi-Fi 触发同步时自动连接,通过isBluetoothConnected与isWifiP2PConnected监听状态。
- 优化蓝牙扫描策略:基于 CXR-M SDK 的BluetoothHelper过滤 Rokid 设备 UUID,减少无效扫描消耗,提升连接速度。
难点 2:条码识别准确率与速度平衡
问题:快件条码可能存在污损、褶皱,需兼顾识别速度与准确率。解决方案:
- 相机参数优化:通过setPhotoParams调整分辨率与压缩质量,在不影响识别的前提下降低图像传输延迟。
- 本地 + 云端双识别机制:眼镜端本地优先识别(CXR-S SDK 图像处理能力),失败后 300ms 内自动触发云端识别,保障流程不中断。
难点 3:多指令并发处理
问题:工作人员可能连续触发 “扫描”“归类”“查询” 等指令,需避免指令冲突。解决方案:
- 指令队列管理:手机端维护指令优先级队列,语音指令与视觉操作指令分类处理,高优先级指令(如扫描确认)优先执行。
- 状态反馈机制:通过提词器实时显示当前操作状态(如 “扫描中”“同步中”),避免重复触发。
五、结语:让技术提升工作体验
通过项目实践,我们在设备协同、场景配置与异常处理等方面积累了重要经验。在设备协同方面,总结出“蓝牙保活 + Wi-Fi 同步”的双模通信方案,并借助CXR-M SDK的deinitBluetooth与deinitWifiP2P接口优化资源释放逻辑,有效降低了设备功耗。在场景适配方面,提炼出快递场景专属的提词器配置模板与相机参数组合,为同类物流场景提供了可直接复用的配置基础。在系统稳定性方面,形成了涵盖设备断连、识别失败、网络中断等8类常见异常的标准化处理流程,并基于SDK回调接口构建了快速恢复机制,提升了系统的鲁棒性。
着眼于未来应用,我们持续推进技术融合与功能优化。在AI能力方面,引入Rokid AI大模型,实现了快件破损识别与收件人信息脱敏处理,进一步提升了业务的智能化水平。在多语言支持方面,利用翻译场景接口(sendTranslationContent)适配国际快件场景,支持多语言语音指令与信息显示,拓展了系统的适用范围。在设备管理方面,基于CXR-M SDK的设备状态监听接口(如BatteryLevelUpdateListener),实现了眼镜端电量、亮度等关键状态的远程管理,为设备的持续稳定运行提供了有力保障。
综上,本次技术提升工作不仅沉淀了多项可复用的实践经验,也通过持续迭代拓展了系统的智能化边界与应用场景。未来,我们将继续深化AI与业务场景的融合,优化设备协同与资源管理机制,为物流行业数智化升级提供更可靠、高效的技术支撑。
更多推荐



所有评论(0)