【征文计划】基于 CXR-M SDK 打造 “AR 眼镜 + 手机” 户外步徒协同导航系统

前言

户外徒步中,传统导航存在低头看手机致险、岔路记录难关联位置、紧急情况环境信息同步难等问题。CXR-M SDK 可实现手机与 Rokid Glasses 稳定连接,调用录音、拍照等能力,接入 YodaOS-Sprite 交互流程,但户外开发需解决蓝牙断连、强光显示不清、离线同步失败等问题。本文围绕 "户外徒步协同导航系统",涵盖 SDK 环境搭建、功能落地及避坑指南,助力开发者快速转化 SDK 能力为实用功能。

CXR-M SDK 核心能力与适用范围

CXR-M SDK 是移动端开发工具包(仅 Android 版本),用于构建手机与 Rokid Glasses 的控制协同应用,支持数据通信、实时音视频获取及场景自定义,可接入 YodaOS-Sprite 交互流程,调用文件互传、录音、拍照等 Rokid Assist Service 服务。

官方文档参考: custom.rokid.com/prod/rokid_...

CXR-M SDK 导入配置指南

以 Kotlin DSL(build.gradle.kts)为例,从三方面说明配置流程:

1.Maven 仓库配置:资源拉取基础

<font style="color:rgba(0, 0, 0, 0.85);">settings.gradle.kts</font><font style="color:rgba(0, 0, 0, 0.85);">dependencyResolutionManagement</font>节点中添加 Rokid Maven 仓库,保留基础仓库:

kotlin 复制代码
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 仓库,用于拉取 CXR-M SDK
        maven { url = uri("https://maven.rokid.com/repository/maven-public/") }
        google()
        mavenCentral()
    }
}

2.SDK 与依赖导入:版本兼容与环境要求

模块级<font style="color:rgba(0, 0, 0, 0.85);">build.gradle.kts</font>中添加核心依赖,设置<font style="color:rgba(0, 0, 0, 0.85);">minSdk=28</font>(最低支持 Android 9.0),并导入配套依赖(版本冲突时优先使用 SDK 版本):

kotlin 复制代码
android {
    // 其他基础配置(如 compileSdk、buildToolsVersion 等)
    defaultConfig {
        // 其他配置(如 applicationId、targetSdk 等)
        minSdk = 28 // 必须设置为 28 及以上,否则不支持 SDK
    }
    // 其他配置(如 buildTypes、compileOptions 等)
}

dependencies {
    // 其他项目依赖(如 AndroidX、第三方库等)
    
    // 1. CXR-M SDK 核心依赖
    implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2")
    
    // 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")
}

3.权限申请:静态声明与动态校验双保障

CXR-M SDK 依赖网络、Wi-Fi、蓝牙(含定位关联权限)等能力,需完成 静态权限声明动态权限申请,否则 SDK 无法正常使用:

1. 静态权限声明

在项目的 <font style="color:rgba(0, 0, 0, 0.85) !important;background-color:rgba(0, 0, 0, 0);">AndroidManifest.xml</font> 中声明 SDK 所需的 "最小权限集",涵盖网络、Wi-Fi、蓝牙及定位相关权限,代码如下:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- 蓝牙相关权限(含 Android S 及以上新增权限) -->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    <!-- 蓝牙依赖的定位权限 -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <!-- 网络与 Wi-Fi 权限 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

    <application>
        <!-- 项目其他配置(如 Activity 注册、Application 声明等) -->
    </application>
</manifest>

2. 动态权限申请

Android 6.0(API 23)及以上需动态申请危险权限,CXR-M SDK 使用前必须确保所有必要权限已授予(权限不足会导致 SDK 功能不可用)。以下是基于 <font style="color:rgba(0, 0, 0, 0.85) !important;background-color:rgba(0, 0, 0, 0);">Activity</font> 的动态申请示例:

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    companion object {
        private const val TAG = "MainActivity"
        private const val REQUEST_CODE_PERMISSIONS = 100 // 权限请求码
        // 必要权限列表(区分 Android S/API 31 及以上的新增蓝牙权限)
        private val REQUIRED_PERMISSIONS = mutableListOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.BLUETOOTH,
            Manifest.permission.BLUETOOTH_ADMIN
        ).apply {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                add(Manifest.permission.BLUETOOTH_SCAN)
                add(Manifest.permission.BLUETOOTH_CONNECT)
            }
        }.toTypedArray()
    }

    // 用于观察权限申请结果的 LiveData
    private val isAllPermissionsGranted = MutableLiveData<Boolean?>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 初始化时发起权限申请
        isAllPermissionsGranted.postValue(null)
        requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)

        // 观察权限申请结果,决定是否初始化 SDK
        isAllPermissionsGranted.observe(this) { granted ->
            when (granted) {
                true -> {
                    // 所有权限已授予,可初始化 CXR-M SDK
                }
                false -> {
                    // 部分权限被拒绝,需提示用户开启(否则 SDK 不可用)
                }
            }
        }
    }

    // 接收权限申请结果
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            // 判断所有权限是否均被授予
            val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
            isAllPermissionsGranted.postValue(allGranted)
        }
    }
}

户外徒步场景需求拆解与技术方案

1. 前置准备:SDK 导入与户外场景权限优化

(1)SDK 依赖与系统配置优化

依赖配置代码新增高德定位 SDK 和低功耗蓝牙库,设<font style="color:rgba(0, 0, 0, 0.85);">manifestPlaceholders["ble_low_power"] = "true"</font>;权限代码补充后台定位、存储权限,用弹窗强制引导开启。优化围绕户外场景,保障导航必需的定位能力与设备续航。

kotlin 复制代码
// build.gradle.kts(模块级)
dependencies {
    // CXR-M SDK核心依赖(固定版本,避免户外环境下版本兼容问题)
    implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2")
    // 文档要求的基础依赖(Retrofit、OkHttp等)
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.okhttp3:okhttp:4.9.3")
    // 新增:户外定位依赖(高德地图SDK)
    implementation("com.amap.api:location:5.6.1")
    implementation("com.amap.api:map2d:5.2.0")
    // 新增:低功耗蓝牙适配库(延长户外设备续航)
    implementation("androidx.bluetooth:bluetooth-le:1.1.0")
}

android {
    defaultConfig {
        minSdk = 28 // 遵循SDK要求
        targetSdk = 33
        // 新增:户外场景下的蓝牙低功耗配置
        manifestPlaceholders["ble_low_power"] = "true"
    }
    // 优化:户外高温环境下的编译配置(减少资源占用)
    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
}

(2)户外场景权限强化

除文档中的蓝牙、网络、存储权限外,新增定位权限 (导航必需)和后台蓝牙权限(户外远距离时维持连接),并优化动态申请逻辑 ------ 户外场景下,权限拒绝会直接影响安全,需强制引导开启:

kotlin 复制代码
class OutdoorPermissionManager(private val activity: AppCompatActivity) {
    // 户外场景必需权限:定位(导航)+ 蓝牙(连接)+ 存储(离线数据)
    private val REQUIRED_PERMISSIONS = mutableListOf(
        Manifest.permission.ACCESS_FINE_LOCATION, // 精确定位(徒步路线需要)
        Manifest.permission.ACCESS_BACKGROUND_LOCATION, // 后台定位(持续导航)
        Manifest.permission.BLUETOOTH_SCAN,
        Manifest.permission.BLUETOOTH_CONNECT,
        Manifest.permission.WRITE_EXTERNAL_STORAGE // 存储离线地图
    ).apply {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            add(Manifest.permission.BLUETOOTH_ADVERTISE) // 安卓13+后台蓝牙广播
        }
    }.toTypedArray()

    // 权限申请结果回调
    var onPermissionReady: (() -> Unit)? = null

    fun requestOutdoorPermissions() {
        if (isAllPermissionsGranted()) {
            onPermissionReady?.invoke()
            return
        }
        // 发起动态申请,重点提示定位权限的必要性
        ActivityResultContracts.RequestMultiplePermissions().launch(
            activity,
            REQUIRED_PERMISSIONS
        ) { resultMap ->
            val allGranted = resultMap.values.all { it }
            if (!allGranted) {
                // 户外场景下强制引导:权限不足无法导航,弹出设置页引导
                AlertDialog.Builder(activity)
                    .setTitle("户外导航必需权限")
                    .setMessage("定位、蓝牙权限用于路线同步和设备连接,拒绝会导致无法使用导航功能,请前往设置开启")
                    .setCancelable(false) // 不可取消,确保权限开启
                    .setPositiveButton("去设置") { _, _ ->
                        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
                            data = Uri.fromParts("package", activity.packageName, null)
                        }
                        activity.startActivityForResult(intent, 1001)
                    }
                    .show()
            } else {
                onPermissionReady?.invoke()
            }
        }
    }

    // 检查所有权限是否已授予
    private fun isAllPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(activity, it) == PackageManager.PERMISSION_GRANTED
    }

    // 从设置页返回后重新检查权限
    fun onSettingsReturn() {
        if (isAllPermissionsGranted()) {
            onPermissionReady?.invoke()
        } else {
            requestOutdoorPermissions() // 仍未开启则重新提示
        }
    }
}

2. 核心功能实现:户外场景下的设备协同

(1)设备连接优化:户外低功耗蓝牙适配

初始化 SDK 时配置<font style="color:rgba(0, 0, 0, 0.85);">BluetoothMode.BLE_LOW_POWER</font>和 3 秒重连间隔,扫描时过滤 Rokid Glasses,连接后监听信号(弱信号提示)并同步户外显示参数。通过低功耗与自动重连,解决户外设备距离波动与续航问题。

kotlin 复制代码
class RokidOutdoorDeviceManager(private val context: Context) {
    private lateinit var cxrClient: CxrClient
    private var targetDeviceId: String? = null
    private var isLowPowerMode = true // 户外默认开启低功耗模式

    // 初始化SDK并开启低功耗蓝牙连接
    fun initOutdoorClient(
        appKey: String,
        appSecret: String,
        accessKey: String,
        onInitResult: ((Boolean) -> Unit)
    ) {
        // 初始化CXR-M SDK,传入户外场景配置(低功耗)
        CxrClient.init(
            context = context,
            appKey = appKey,
            appSecret = appSecret,
            accessKey = accessKey,
            config = CxrConfig.Builder()
                .setBluetoothMode(if (isLowPowerMode) BluetoothMode.BLE_LOW_POWER else BluetoothMode.NORMAL)
                .setReconnectInterval(3000) // 信号弱时3秒重试一次
                .build()
        ) { initSuccess ->
            if (!initSuccess) {
                onInitResult.invoke(false)
                return@init
            }
            cxrClient = CxrClient.getInstance()
            onInitResult.invoke(true)
        }
    }

    // 扫描并连接Rokid Glasses(户外场景下过滤非徒步用设备)
    fun scanAndConnectOutdoorDevice(onConnectResult: ((Boolean, String?) -> Unit)) {
        cxrClient.scanDevices(
            scanMode = ScanMode.LOW_POWER, // 低功耗扫描,节省电量
            filter = { device -> 
                // 只连接Rokid Glasses(名称含"Rokid Glasses"),排除其他设备
                device.name.contains("Rokid Glasses") && device.isOutdoorSupported() 
            },
            callback = object : DeviceScanCallback {
                override fun onDeviceFound(device: RokidDevice) {
                    targetDeviceId = device.deviceId
                    // 连接时开启"信号强度监听",弱信号时提示用户调整设备位置
                    cxrClient.connectDevice(
                        device = device,
                        signalListener = { rssi ->
                            if (rssi < -80) { // RSSI低于-80为弱信号
                                Toast.makeText(context, "蓝牙信号弱,请靠近手机或调整设备位置", Toast.LENGTH_SHORT).show()
                            }
                        },
                        connectCallback = object : ConnectCallback {
                            override fun onConnected() {
                                // 连接成功后,同步眼镜的户外显示参数(如亮度)
                                syncOutdoorDisplayConfig()
                                onConnectResult.invoke(true, null)
                            }

                            override fun onDisconnected() {
                                onConnectResult.invoke(false, "设备断开连接,正在重试...")
                                // 自动重连
                                targetDeviceId?.let { cxrClient.reconnectDevice(it) }
                            }

                            override fun onConnectFailed(errorMsg: String) {
                                onConnectResult.invoke(false, "连接失败:$errorMsg")
                            }
                        }
                    )
                }

                override fun onScanFailed(errorMsg: String) {
                    onConnectResult.invoke(false, "扫描设备失败:$errorMsg")
                }
            }
        )
    }

    // 同步户外显示配置:强光下提高眼镜亮度,适配户外可视性
    private fun syncOutdoorDisplayConfig() {
        targetDeviceId?.let { deviceId ->
            // 调用YodaOS-Sprite的显示接口,设置户外模式(亮度70%,抗眩光)
            cxrClient.setDeviceConfig(
                deviceId = deviceId,
                configType = ConfigType.DISPLAY,
                configParams = mapOf(
                    "brightness" to 70, // 亮度70%(默认50%,户外需提高)
                    "antiGlare" to true, // 开启抗眩光
                    "outdoorMode" to true // 开启户外显示优化
                ),
                callback = object : ConfigCallback {
                    override fun onConfigSuccess() {
                        Log.d("OutdoorConfig", "眼镜户外显示参数同步成功")
                    }

                    override fun onConfigFailed(errorMsg: String) {
                        Log.e("OutdoorConfig", "显示参数同步失败:$errorMsg")
                    }
                }
            )
        }
    }

    // 获取当前眼镜状态(电量、存储)
    fun getGlassesStatus(onStatusReady: ((GlassesStatus) -> Unit)) {
        targetDeviceId?.let { deviceId ->
            cxrClient.getDeviceStatus(
                deviceId = deviceId,
                statusTypes = listOf(StatusType.BATTERY, StatusType.STORAGE, StatusType.NETWORK),
                callback = object : StatusCallback {
                    override fun onStatusReceived(statusMap: Map<StatusType, Any>) {
                        val status = GlassesStatus(
                            battery = statusMap[StatusType.BATTERY] as Int, // 电量百分比
                            storageLeft = statusMap[StatusType.STORAGE] as Long, // 剩余存储(MB)
                            networkType = statusMap[StatusType.NETWORK] as String // 网络类型(4G/Wi-Fi)
                        )
                        onStatusReady.invoke(status)
                    }
                }
            )
        }
    }

    // 眼镜状态数据类
    data class GlassesStatus(
        val battery: Int, // 电量(0-100)
        val storageLeft: Long, // 剩余存储(MB)
        val networkType: String // 网络类型
    )
}

(2)核心功能 1:路线规划与 AR 同步

代码调用高德地图 SDK 规划离线 / 在线路线,解析后转 JSON,通过 SDK<font style="color:rgba(0, 0, 0, 0.85);">transferData</font>飞传至眼镜,再调用<font style="color:rgba(0, 0, 0, 0.85);">triggerSceneInteraction</font>触发 AR 叠加显示。实现手机路线数据到眼镜第一视角指引的流转,核心是 SDK 的数据传输与场景交互能力。

kotlin 复制代码
class HikingRouteManager(
    private val cxrClient: CxrClient,
    private val deviceId: String,
    private val amapLocationClient: AMapLocationClient
) {
    // 规划徒步路线(支持离线模式)
    fun planHikingRoute(
        startPoint: LatLng, // 起点(手机定位获取)
        endPoint: LatLng, // 终点(用户在手机地图选择)
        isOffline: Boolean, // 是否离线模式
        onRoutePlanned: ((HikingRoute) -> Unit)
    ) {
        // 1. 调用高德地图SDK规划徒步路线
        val routeSearch = RouteSearch(context)
        val routeQuery = RouteSearch.WalkRouteQuery(
            WalkRouteSearchRequest().apply {
                origin = RouteSearch.Point(startPoint.longitude, startPoint.latitude)
                destination = RouteSearch.Point(endPoint.longitude, endPoint.latitude)
                mode = if (isOffline) RouteSearch.MODE_WALK_OFFLINE else RouteSearch.MODE_WALK_ONLINE
            }
        )

        routeSearch.calculateWalkRouteAsyn(routeQuery)
        routeSearch.setRouteSearchListener(object : RouteSearch.OnRouteSearchListener {
            override fun onWalkRouteSearched(result: WalkRouteResult, errorCode: Int) {
                if (errorCode != 1000 || result.walkPaths.isEmpty()) {
                    Toast.makeText(context, "路线规划失败,请检查地图数据", Toast.LENGTH_SHORT).show()
                    return
                }
                // 2. 解析路线数据(提取关键节点:岔路口、转弯点)
                val walkPath = result.walkPaths[0]
                val routeNodes = walkPath.steps.map { step ->
                    RouteNode(
                        latLng = LatLng(step.polyline[0].latitude, step.polyline[0].longitude),
                        instruction = step.instruction, // 导航指令(如"左转50米")
                        distance = step.distance // 距离(米)
                    )
                }
                val hikingRoute = HikingRoute(
                    totalDistance = walkPath.distance,
                    totalTime = walkPath.duration,
                    nodes = routeNodes
                )
                // 3. 同步路线数据到AR眼镜
                syncRouteToGlasses(hikingRoute)
                onRoutePlanned.invoke(hikingRoute)
            }

            override fun onBusRouteSearched(p0: BusRouteResult?, p1: Int) {}
            override fun onDriveRouteSearched(p0: DriveRouteResult?, p1: Int) {}
            override fun onRideRouteSearched(p0: RideRouteResult?, p1: Int) {}
        })
    }

    // 同步路线数据到AR眼镜(通过CXR-M SDK飞传JSON格式数据)
    private fun syncRouteToGlasses(route: HikingRoute) {
        // 1. 将路线数据转为JSON字符串
        val routeJson = Gson().toJson(route)
        // 2. 用SDK的"数据飞传"功能发送到眼镜(非文件,轻量数据)
        cxrClient.transferData(
            deviceId = deviceId,
            dataType = DataType.ROUTE, // 自定义数据类型:路线
            data = routeJson.toByteArray(),
            callback = object : DataTransferCallback {
                override fun onTransferSuccess() {
                    Log.d("RouteSync", "路线数据同步到眼镜成功")
                    // 同步后,触发眼镜的AR显示(调用YodaOS-Sprite场景交互)
                    triggerGlassesRouteDisplay()
                }

                override fun onTransferFailed(errorMsg: String) {
                    Toast.makeText(context, "路线同步失败:$errorMsg", Toast.LENGTH_SHORT).show()
                }
            }
        )
    }

    // 触发眼镜的AR路线显示(基于YodaOS-Sprite的场景交互)
    private fun triggerGlassesRouteDisplay() {
        cxrClient.triggerSceneInteraction(
            deviceId = deviceId,
            sceneType = SceneType.HIKING_NAV, // 徒步导航场景
            interactionParams = mapOf("displayMode" to "AR_OVERLAY"), // AR叠加显示
            callback = object : SceneCallback {
                override fun onInteractionSuccess() {
                    Toast.makeText(context, "眼镜AR导航已开启", Toast.LENGTH_SHORT).show()
                }

                override fun onInteractionFailed(errorMsg: String) {
                    Log.e("ARDisplay", "AR路线显示失败:$errorMsg")
                }
            }
        )
    }

    // 路线数据类
    data class HikingRoute(
        val totalDistance: Int, // 总距离(米)
        val totalTime: Int, // 总时间(秒)
        val nodes: List<RouteNode> // 路线节点
    )

    // 路线节点数据类
    data class RouteNode(
        val latLng: LatLng, // 经纬度
        val instruction: String, // 导航指令
        val distance: Int // 距离下一个节点的距离(米)
    )
}

(3)核心功能 2:环境标记与紧急协同

环境标记代码调用 SDK<font style="color:rgba(0, 0, 0, 0.85);">takePhoto</font>(关美颜、高清模式),回传后关联定位标记;紧急协同代码先<font style="color:rgba(0, 0, 0, 0.85);">startRecording</font>录 10 秒语音,再拍照,组装数据用<font style="color:rgba(0, 0, 0, 0.85);">transferFile</font>飞传同伴。依赖 SDK 音视频采集与文件飞传能力,满足户外场景需求。

kotlin 复制代码
class OutdoorAssistManager(
    private val cxrClient: CxrClient,
    private val deviceId: String,
    private val currentLocation: () -> LatLng // 获取当前手机定位
) {
    // 1. 环境标记:眼镜拍照回传手机,标记在地图
    fun markEnvironmentPoint(tag: String, onMarkSuccess: ((String) -> Unit)) {
        // 调用SDK拍照功能(户外场景:关闭美颜,提高画质用于识别)
        cxrClient.takePhoto(
            deviceId = deviceId,
            photoConfig = PhotoConfig(
                beautyMode = false,
                resolution = Resolution.HD, // 高清模式,便于看清岔路细节
                saveToLocal = true // 同时保存到手机本地
            ),
            callback = object : PhotoCallback {
                override fun onPhotoTaken(photoPath: String) {
                    // 回传手机后,获取当前定位,标记在地图
                    val location = currentLocation.invoke()
                    val markMsg = "已标记【$tag】在(${location.latitude}, ${location.longitude})"
                    Toast.makeText(context, markMsg, Toast.LENGTH_SHORT).show()
                    onMarkSuccess.invoke(photoPath)
                }

                override fun onPhotoFailed(errorMsg: String) {
                    Toast.makeText(context, "环境拍照失败:$errorMsg", Toast.LENGTH_SHORT).show()
                }
            }
        )
    }

    // 2. 紧急协同:录音+拍照,飞传给同伴设备
    fun sendEmergencyHelp(companionDeviceId: String, onHelpSent: ((Boolean) -> Unit)) {
        // 第一步:启动录音(10秒,记录求助语音)
        cxrClient.startRecording(
            deviceId = deviceId,
            audioConfig = AudioConfig(
                duration = 10, // 固定10秒紧急录音
                format = AudioFormat.MP3,
                sampleRate = 44100 // 高采样率,确保语音清晰
            ),
            callback = object : RecordingCallback {
                override fun onRecordingStopped(audioPath: String) {
                    // 录音完成后,自动拍照(当前环境)
                    takeEmergencyPhoto(audioPath, companionDeviceId, onHelpSent)
                }

                override fun onRecordingFailed(errorMsg: String) {
                    Toast.makeText(context, "紧急录音失败:$errorMsg", Toast.LENGTH_SHORT).show()
                    onHelpSent.invoke(false)
                }
            }
        )
    }

    // 紧急拍照并飞传
    private fun takeEmergencyPhoto(
        audioPath: String,
        companionDeviceId: String,
        onHelpSent: ((Boolean) -> Unit)
    ) {
        cxrClient.takePhoto(
            deviceId = deviceId,
            photoConfig = PhotoConfig(resolution = Resolution.FHD), // 全高清拍照
            callback = object : PhotoCallback {
                override fun onPhotoTaken(photoPath: String) {
                    // 组装紧急求助包:照片+录音+当前定位
                    val helpData = EmergencyHelpData(
                        photoPath = photoPath,
                        audioPath = audioPath,
                        location = currentLocation.invoke(),
                        timestamp = System.currentTimeMillis()
                    )
                    // 飞传给同伴设备
                    cxrClient.transferFile(
                        sourceDeviceId = deviceId,
                        targetDeviceId = companionDeviceId,
                        filePath = Gson().toJson(helpData).toByteArray(), // 转为JSON文件
                        fileType = FileType.EMERGENCY_HELP,
                        callback = object : FileTransferCallback {
                            override fun onTransferSuccess() {
                                Toast.makeText(context, "紧急求助已发送", Toast.LENGTH_SHORT).show()
                                onHelpSent.invoke(true)
                            }

                            override fun onTransferFailed(errorMsg: String) {
                                Toast.makeText(context, "求助发送失败:$errorMsg", Toast.LENGTH_SHORT).show()
                                onHelpSent.invoke(false)
                            }
                        }
                    )
                }

                override fun onPhotoFailed(errorMsg: String) {
                    Toast.makeText(context, "紧急拍照失败:$errorMsg", Toast.LENGTH_SHORT).show()
                    onHelpSent.invoke(false)
                }
            }
        )
    }

    // 紧急求助数据类
    data class EmergencyHelpData(
        val photoPath: String, // 环境照片路径
        val audioPath: String, // 求助录音路径
        val location: LatLng, // 当前定位
        val timestamp: Long // 时间戳
    )
}

3. 设备状态监控:户外续航与存储兜底

代码用<font style="color:rgba(0, 0, 0, 0.85);">CoroutineScope(Dispatchers.IO)</font>定时(1 分钟)调用<font style="color:rgba(0, 0, 0, 0.85);">getGlassesStatus</font>,电量低于 20%、存储低于 100MB 或无网络时,顶部弹窗预警。通过定时检查与可视化提示,提前规避眼镜断电、存储不足导致的导航中断

kotlin 复制代码
class OutdoorStatusMonitor(
    private val deviceManager: RokidOutdoorDeviceManager,
    private val context: Context
) {
    private val statusCheckInterval = 60000 // 1分钟检查一次状态

    // 启动状态监控
    fun startStatusMonitor() {
        CoroutineScope(Dispatchers.IO).launch {
            while (true) {
                delay(statusCheckInterval.toLong())
                deviceManager.getGlassesStatus { status ->
                    // 1. 电量预警:低于20%提示充电
                    if (status.battery < 20) {
                        showWarning("眼镜电量不足20%,请尽快充电")
                    }
                    // 2. 存储预警:剩余存储低于100MB提示清理
                    if (status.storageLeft < 100) {
                        showWarning("眼镜剩余存储不足100MB,建议删除无用照片/录音")
                    }
                    // 3. 网络预警:无网络时提示切换离线模式
                    if (status.networkType == "NONE") {
                        showWarning("眼镜无网络连接,已自动切换到离线导航模式")
                    }
                }
            }
        }
    }

    // 显示预警弹窗(户外场景:弹窗不遮挡导航界面,放在顶部)
    private fun showWarning(message: String) {
        val toast = Toast.makeText(context, message, Toast.LENGTH_LONG)
        toast.setGravity(Gravity.TOP, 0, 100) // 顶部显示,避免遮挡地图
        toast.show()
    }
}

总结

本文基于 CXR-M SDK 落地户外徒步协同导航系统,解决四大痛点,凸显 SDK "场景连接器" 价值。同时沉淀户外开发要点:优先做环境适配,场景化调优 SDK 功能,完善风险兜底机制。还分享避坑逻辑,且系统架构可复用到多户外场景。开发者学习 SDK 不应局限接口调用,需结合场景痛点转化价值,本文经验助力开发者少走弯路,快速落地高质量 AR 协同应用。

相关推荐
rengang666 小时前
08-决策树:探讨基于树结构的分类和回归方法及其优缺点
人工智能·算法·决策树·机器学习·分类·回归
闻缺陷则喜何志丹6 小时前
【剪枝 贪心 回溯】B4093 [CSP-X2021 山东] 发送快递|普及+
c++·算法·剪枝·贪心·洛谷
猫头虎7 小时前
HAMi 2.7.0 发布:全面拓展异构芯片支持,优化GPU资源调度与智能管理
嵌入式硬件·算法·prompt·aigc·embedding·gpu算力·ai-native
漫漫不慢.7 小时前
算法练习-二分查找
java·开发语言·算法
如竟没有火炬7 小时前
LRU缓存——双向链表+哈希表
数据结构·python·算法·leetcode·链表·缓存
Greedy Alg7 小时前
LeetCode 236. 二叉树的最近公共祖先
算法
爱吃生蚝的于勒7 小时前
【Linux】零基础学会Linux之权限
linux·运维·服务器·数据结构·git·算法·github
兮山与8 小时前
算法3.0
算法
爱编程的化学家8 小时前
代码随想录算法训练营第27天 -- 动态规划1 || 509.斐波那契数列 / 70.爬楼梯 / 746.使用最小花费爬楼梯
数据结构·c++·算法·leetcode·动态规划·代码随想录