一、创意缘起:工作堆积如山,科技顺其有序

场景:
下午 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 接口initWifiP2PisWifiP2PConnected实现状态管理。
/**
 * 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 SDKisBluetoothConnected定期检查连接状态,闲置时维持低功耗连接,避免频繁断连。
  • 重连逻辑:断开后 3 秒内自动调用connectBluetooth复用savedUuidsavedMac重连,3 次失败后触发语音提醒工作人员。
 Wi-Fi P2P 自动触发
  • 触发条件:当检测到需同步文件(如条码图像、快件信息)时,自动调用initWifiP2P初始化 Wi-Fi 连接,同步完成后 30 秒自动释放资源。
  • 状态监听:通过isWifiP2PConnected判断 Wi-Fi 状态,未连接时缓存数据,连接后自动触发同步。
2. 快件信息采集与识别
条码扫描实现
利用眼镜端相机接口实现条码快速识别,配合 AI 优化识别算法:
  1. 相机配置:通过 CXR-M SDK setPhotoParams设置扫描分辨率(推荐 1920x1080),调用openGlassCamera打开眼镜端相机,takeGlassPhoto拍摄条码图像。
  2. 本地识别:眼镜端通过 CXR-S SDK 的图像识别能力解析条码信息,若本地识别失败,将图像通过 Wi-Fi 同步至手机端进行云端识别。
  3. 信息校验:手机端接收条码信息后,调用云端 API 校验快件单号合法性、收件人信息完整性,通过提词器场景(setWordTipsText)在眼镜端显示校验结果。
语音指令交互
基于 Rokid 语音识别能力,支持以下核心指令:
  • 主动触发:“扫描快件”“确认归类”“查询库存” 等操作指令。
  • 被动反馈:眼镜端通过 TTS 接口(sendTTSContent)播报 “扫描成功”“请归类至 A 区 3 号架” 等反馈信息。
3. 智能归类与指引
归类规则引擎
  1. 云端配置:快递站根据区域、收件人地址、快件类型预设归类规则(如 “同城件→A 区”“大件→B 区”)。
  2. 实时匹配:手机端接收快件信息后,调用云端 API 获取归类结果,通过sendStream接口将指引信息推送至眼镜端。
  3. 视觉指引:在眼镜端自定义界面(openCustomView)显示归类区域示意图,配合语音播报完成精准分拣。
异常处理机制
  • 条码识别失败:语音提示 “请调整角度重新扫描”,并在提词器显示操作指引。
  • 归类规则不存在:自动标记为 “待人工处理”,同步至管理系统并提醒工作人员。
  • 网络中断:数据缓存至手机端(sendStream临时存储),网络恢复后自动同步(startSync)。
4. 数据实时同步与管理
  1. 本地缓存:手机端通过 CXR-M SDK 的sendStream接口缓存快件信息与图像,保障离线状态下的操作连续性。
  2. 云端同步:Wi-Fi 连接状态下,调用startSync接口将缓存数据同步至云端,支持单个文件同步(syncSingleFile)与批量同步。
  3. 状态监听:通过MediaFilesUpdateListener监听眼镜端媒体文件更新,确保扫描图像无遗漏同步。

四、核心难点与解决方案

难点 1:移动场景下设备连接稳定性

问题:快递站空间大、人员移动频繁,蓝牙连接易受干扰,Wi-Fi 切换需无缝衔接。解决方案:
  • 实现蓝牙与 Wi-Fi 双模自动切换:蓝牙负责日常指令传输,Wi-Fi 触发同步时自动连接,通过isBluetoothConnectedisWifiP2PConnected监听状态。
  • 优化蓝牙扫描策略:基于 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与业务场景的融合,优化设备协同与资源管理机制,为物流行业数智化升级提供更可靠、高效的技术支撑。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐