《“慧眼识障“:基于Rokid AI眼镜的智能维修记录自动归档系统开发实战》

摘要

本文详细阐述了如何利用Rokid CXR-M SDK开发一套面向工业维修场景的智能维修记录自动归档系统。该系统通过Rokid AI眼镜与手机端协同工作,实现维修过程的语音记录、实时拍照录像、AI辅助诊断、数据自动同步和云端归档,彻底解决了传统维修记录手工填写效率低、易出错、难追溯等问题。文章从系统架构设计、开发环境搭建到核心功能实现,提供了完整的代码示例和最佳实践,为工业智能化转型提供了可落地的技术方案。

1. 引言:维修记录管理的现状与挑战

在现代工业生产中,设备维修是保障生产线正常运行的关键环节。然而,传统的维修记录方式仍存在诸多痛点:

  • 手工记录效率低下:维修工程师需要在完成维修后,花费大量时间填写纸质工单
  • 信息不完整:关键细节、故障现象容易被遗漏,影响后续分析
  • 追溯困难:纸质记录难以长期保存,查询历史维修记录耗时费力
  • 知识传承断层:资深工程师的经验无法有效沉淀和传承
  • 数据价值未挖掘:维修数据分散,无法进行大数据分析预测设备故障

随着工业4.0和智能制造的发展,智能穿戴设备为解决这些问题提供了新思路。Rokid AI眼镜凭借其轻便、免提操作、AR显示等优势,成为工业维修场景的理想选择。本文将详细讲解如何基于Rokid CXR-M SDK,构建一套完整的智能维修记录自动归档系统。

2. 系统架构设计

2.1 整体架构

系统采用"端-边-云"三层架构设计:

  • 终端层:Rokid AI眼镜作为数据采集终端,负责语音输入、图像拍摄、AR显示
  • 边缘层:手机APP作为控制中心,处理数据同步、用户交互、本地存储
  • 云端层:服务器负责数据存储、分析处理、知识沉淀、AI模型训练

2.2 技术选型

|------|--------------------------|-----------------------|
| 模块 | 技术方案 | 优势 |
| 设备连接 | Rokid CXR-M SDK | 稳定的蓝牙/WiFi连接,完善的API支持 |
| 语音处理 | 阿里云智能语音 | 高准确率,支持工业术语 |
| 图像处理 | OpenCV + TensorFlow Lite | 轻量级,适合移动端部署 |
| 数据存储 | Room + Firebase | 本地缓存+云端同步,离线可用 |
| 报告生成 | Apache POI | 丰富的文档格式支持 |
| 云端服务 | Spring Boot + MySQL | 成熟稳定,易于扩展 |

2.3 Rokid CXR-M SDK核心功能

本系统充分利用Rokid CXR-M SDK的以下核心功能:

  • 设备连接管理:蓝牙/WiFi双模连接,确保数据传输稳定性
  • 自定义AI场景:构建维修专用AI助手,提供故障诊断建议
  • 多媒体采集:高质量拍照录像,记录维修关键步骤
  • 提词器功能:显示维修步骤指引,确保操作规范
  • 数据同步:自动将维修记录同步至云端,实现无缝归档

3. 开发环境搭建

3.1 SDK导入

首先需要在Android项目中导入Rokid CXR-M SDK。按照文档要求,我们需要配置Maven仓库和依赖项:

复制代码
// settings.gradle.kts
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        maven { url = uri("https://maven.rokid.com/repository/maven-public/") }
        google()
        mavenCentral()
    }
}

// build.gradle.kts
android {
    defaultConfig {
        minSdk = 28
        // 其他配置...
    }
}

dependencies {
    implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2")
    // 其他依赖
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    implementation("com.google.code.gson:gson:2.10.1")
    implementation("androidx.room:room-runtime:2.4.0")
    implementation("androidx.room:room-ktx:2.4.0")
    kapt("androidx.room:room-compiler:2.4.0")
}

3.2 权限配置

维修记录系统需要访问多种设备权限,需要在AndroidManifest.xml中声明,并在运行时动态申请:

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

    <!-- 蓝牙权限 -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <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_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    
    <!-- 多媒体权限 -->
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        android:name=".MaintenanceApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
        <!-- Activity声明 -->
    </application>
</manifest>

3.3 蓝牙/WiFi连接实现

维修记录系统需要稳定的设备连接,我们采用先蓝牙后WiFi的连接策略,确保基础通信和高速数据传输:

复制代码
class MaintenanceBluetoothHelper(
    private val context: Context,
    private val connectionCallback: (Boolean) -> Unit
) {
    private lateinit var cxrApi: CxrApi
    private var bluetoothDevice: BluetoothDevice? = null

    init {
        cxrApi = CxrApi.getInstance()
    }

    fun initializeBluetooth() {
        // 检查权限
        if (!checkRequiredPermissions()) {
            connectionCallback(false)
            return
        }

        // 扫描Rokid设备
        val bluetoothHelper = BluetoothHelper(context as AppCompatActivity, 
            { status ->
                when (status) {
                    BluetoothHelper.INIT_STATUS.INIT_END -> startScan()
                }
            },
            { 
                // 设备发现回调
                handleDiscoveredDevices()
            }
        )
        bluetoothHelper.checkPermissions()
    }

    private fun handleDiscoveredDevices() {
        // 从扫描结果中筛选Rokid眼镜
        val glassesDevices = bluetoothHelper.scanResultMap.values.filter { 
            it.name?.contains("Glasses", ignoreCase = true) ?: false 
        }
        
        if (glassesDevices.isNotEmpty()) {
            bluetoothDevice = glassesDevices.first()
            connectToDevice()
        } else {
            // 尝试从已配对设备中查找
            val bondedGlasses = bluetoothHelper.bondedDeviceMap.values.firstOrNull { 
                it.name?.contains("Glasses", ignoreCase = true) ?: false 
            }
            if (bondedGlasses != null) {
                bluetoothDevice = bondedGlasses
                connectToDevice()
            } else {
                connectionCallback(false)
            }
        }
    }

    private fun connectToDevice() {
        val device = bluetoothDevice ?: return
        cxrApi.initBluetooth(context, device, object : BluetoothStatusCallback {
            override fun onConnectionInfo(socketUuid: String?, macAddress: String?, rokidAccount: String?, glassesType: Int) {
                if (!socketUuid.isNullOrEmpty() && !macAddress.isNullOrEmpty()) {
                    // 保存连接信息用于WiFi连接
                    PreferenceManager.getDefaultSharedPreferences(context).edit {
                        putString("socket_uuid", socketUuid)
                        putString("mac_address", macAddress)
                    }
                    connectWifiIfPossible()
                }
            }

            override fun onConnected() {
                Log.d("MaintenanceApp", "蓝牙连接成功")
                connectionCallback(true)
            }

            override fun onDisconnected() {
                Log.d("MaintenanceApp", "蓝牙连接断开")
                connectionCallback(false)
            }

            override fun onFailed(errorCode: ValueUtil.CxrBluetoothErrorCode?) {
                Log.e("MaintenanceApp", "蓝牙连接失败: $errorCode")
                connectionCallback(false)
            }
        })
    }

    private fun connectWifiIfPossible() {
        // 检查WiFi状态
        val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
        if (!wifiManager.isWifiEnabled) {
            (context as AppCompatActivity).runOnUiThread {
                AlertDialog.Builder(context)
                    .setTitle("WiFi连接")
                    .setMessage("为了获得更好的数据传输体验,请开启WiFi")
                    .setPositiveButton("开启") { _, _ ->
                        wifiManager.isWifiEnabled = true
                        initWifiConnection()
                    }
                    .setNegativeButton("稍后") { _, _ ->
                        // 仅使用蓝牙模式
                    }
                    .show()
            }
        } else {
            initWifiConnection()
        }
    }

    private fun initWifiConnection() {
        cxrApi.initWifiP2P(object : WifiP2PStatusCallback {
            override fun onConnected() {
                Log.d("MaintenanceApp", "WiFi P2P连接成功")
                // 设置更高的传输质量
                setHighQualityMediaParams()
            }

            override fun onDisconnected() {
                Log.d("MaintenanceApp", "WiFi P2P连接断开")
            }

            override fun onFailed(errorCode: ValueUtil.CxrWifiErrorCode?) {
                Log.e("MaintenanceApp", "WiFi P2P连接失败: $errorCode")
            }
        })
    }

    private fun setHighQualityMediaParams() {
        // 为维修记录设置高质量拍照参数
        cxrApi.setPhotoParams(4032, 3024) // 最高分辨率
        // 设置录像参数:30秒,30fps,1920x1080
        cxrApi.setVideoParams(30, 30, 1920, 1080, 1)
    }

    private fun checkRequiredPermissions(): Boolean {
        val requiredPermissions = arrayOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.BLUETOOTH,
            Manifest.permission.BLUETOOTH_ADMIN,
            Manifest.permission.CAMERA,
            Manifest.permission.RECORD_AUDIO
        )
        
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            requiredPermissions.plus(Manifest.permission.BLUETOOTH_SCAN)
            requiredPermissions.plus(Manifest.permission.BLUETOOTH_CONNECT)
        }
        
        return requiredPermissions.all { 
            ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        }
    }
}

4. 核心功能实现

4.1 维修场景自定义

基于Rokid CXR-M SDK,我们构建了专门的维修场景,整合了AI助手、提词器和翻译功能:

复制代码
class MaintenanceSceneManager(private val context: Context) {
    private val cxrApi = CxrApi.getInstance()
    private val gson = Gson()

    fun initMaintenanceScene() {
        // 1. 设置自定义AI助手,专门用于维修诊断
        configureMaintenanceAssistant()
        
        // 2. 配置提词器,用于显示维修步骤
        configureMaintenanceTeleprompter()
        
        // 3. 设置翻译功能,方便查看外文设备手册
        configureMaintenanceTranslation()
    }

    private fun configureMaintenanceAssistant() {
        // 设置AI助手的自定义配置
        val assistantConfig = """
        {
            "scene_name": "maintenance_assistant",
            "language": "zh-CN",
            "domain": "industrial_maintenance",
            "prompts": [
                "你是一名专业的工业设备维修专家,擅长诊断机械、电气和液压系统的故障。",
                "请根据用户描述的故障现象,提供可能的故障原因和维修建议。",
                "如果需要更多信息,可以询问具体的设备型号、运行参数或异常现象。"
            ],
            "sensitive_words": ["危险", "高压", "高温"],
            "emergency_protocols": {
                "danger_keywords": ["冒烟", "火花", "异味", "异响"],
                "emergency_response": "检测到危险信号,请立即停止设备运行,断开电源,联系专业人员处理。"
            }
        }
        """.trimIndent()
        
        // 通过自定义页面显示AI助手界面
        val customViewContent = """
        {
          "type": "LinearLayout",
          "props": {
            "layout_width": "match_parent",
            "layout_height": "match_parent",
            "orientation": "vertical",
            "gravity": "center_horizontal",
            "paddingTop": "100dp",
            "paddingBottom": "80dp",
            "backgroundColor": "#FF1E1E1E"
          },
          "children": [
            {
              "type": "TextView",
              "props": {
                "id": "tv_title",
                "layout_width": "wrap_content",
                "layout_height": "wrap_content",
                "text": "维修助手",
                "textSize": "24sp",
                "textColor": "#FF4CAF50",
                "textStyle": "bold",
                "marginBottom": "30dp"
              }
            },
            {
              "type": "TextView",
              "props": {
                "id": "tv_instruction",
                "layout_width": "wrap_content",
                "layout_height": "wrap_content",
                "text": "长按功能键启动语音诊断",
                "textSize": "16sp",
                "textColor": "#FFFFFFFF",
                "marginBottom": "20dp"
              }
            },
            {
              "type": "ImageView",
              "props": {
                "id": "iv_mic",
                "layout_width": "80dp",
                "layout_height": "80dp",
                "name": "maintenance_mic_icon",
                "layout_gravity": "center_horizontal",
                "marginBottom": "40dp"
              }
            },
            {
              "type": "TextView",
              "props": {
                "id": "tv_status",
                "layout_width": "wrap_content",
                "layout_height": "wrap_content",
                "text": "待机中...",
                "textSize": "14sp",
                "textColor": "#FF9E9E9E"
              }
            }
          ]
        }
        """.trimIndent()
        
        // 上传图标资源
        uploadMaintenanceIcons()
        
        // 打开自定义视图
        cxrApi.openCustomView(customViewContent)
        
        // 设置AI事件监听
        cxrApi.setAiEventListener(object : AiEventListener {
            override fun onAiKeyDown() {
                updateCustomViewStatus("正在聆听...")
                // 启动录音
                cxrApi.openAudioRecord(2, "maintenance_assistant") // 使用opus编码
            }
            
            override fun onAiKeyUp() {
                // 停止录音
                cxrApi.closeAudioRecord("maintenance_assistant")
                updateCustomViewStatus("分析中...")
            }
            
            override fun onAiExit() {
                updateCustomViewStatus("已退出")
            }
        })
    }

    private fun uploadMaintenanceIcons() {
        val icons = listOf(
            IconInfo("maintenance_mic_icon", getBase64Icon(R.drawable.ic_mic_white_48dp)),
            IconInfo("maintenance_camera_icon", getBase64Icon(R.drawable.ic_camera_white_48dp)),
            IconInfo("maintenance_check_icon", getBase64Icon(R.drawable.ic_check_circle_white_48dp)),
            IconInfo("maintenance_warning_icon", getBase64Icon(R.drawable.ic_warning_white_48dp))
        )
        cxrApi.sendCustomViewIcons(icons)
    }

    private fun getBase64Icon(resId: Int): String {
        val drawable = ContextCompat.getDrawable(context, resId)
        val bitmap = (drawable as BitmapDrawable).bitmap
        val byteArrayOutputStream = ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
        return Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.DEFAULT)
    }

    private fun updateCustomViewStatus(status: String) {
        val updateJson = """
        [
          {
            "action": "update",
            "id": "tv_status",
            "props": {
              "text": "$status"
            }
          }
        ]
        """.trimIndent()
        cxrApi.updateCustomView(updateJson)
    }

    private fun configureMaintenanceTeleprompter() {
        // 配置提词器参数
        cxrApi.configWordTipsText(
            textSize = 18f,
            lineSpace = 1.5f,
            mode = "normal",
            startPointX = 100,  // 屏幕横向偏移
            startPointY = 200,  // 屏幕纵向偏移
            width = 1000,       // 显示区域宽度
            height = 800        // 显示区域高度
        )
        
        // 打开提词器场景
        cxrApi.controlScene(ValueUtil.CxrSceneType.WORD_TIPS, true, null)
        
        // 设置默认维修步骤
        val defaultSteps = """
        1. 安全确认:断电、挂牌、上锁
        2. 故障现象记录
        3. 部件检查与测试
        4. 问题定位与分析
        5. 维修方案实施
        6. 功能测试与验证
        7. 现场清理与记录
        """.trimIndent()
        
        cxrApi.sendStream(
            ValueUtil.CxrStreamType.WORD_TIPS,
            defaultSteps.toByteArray(),
            "maintenance_steps.txt",
            object : SendStatusCallback {
                override fun onSendSucceed() {
                    Log.d("MaintenanceApp", "提词器内容设置成功")
                }
                override fun onSendFailed(errorCode: ValueUtil.CxrSendErrorCode?) {
                    Log.e("MaintenanceApp", "提词器内容设置失败: $errorCode")
                }
            }
        )
    }

    private fun configureMaintenanceTranslation() {
        // 配置翻译场景,用于查看外文设备手册
        cxrApi.configTranslationText(
            textSize = 16,
            startPointX = 200,
            startPointY = 300,
            width = 800,
            height = 600
        )
        
        // 默认不打开翻译场景,需要时手动触发
    }

    fun startMaintenanceProcess(deviceInfo: MaintenanceDeviceInfo) {
        // 生成维修工单编号
        val workOrderNumber = "WO-${System.currentTimeMillis()}"
        
        // 更新AI助手上下文
        val contextUpdate = """
        {
            "work_order": "$workOrderNumber",
            "device_name": "${deviceInfo.name}",
            "device_model": "${deviceInfo.model}",
            "device_serial": "${deviceInfo.serialNumber}",
            "maintenance_type": "${deviceInfo.maintenanceType}",
            "last_maintenance_date": "${deviceInfo.lastMaintenanceDate ?: "未知"}",
            "known_issues": ${gson.toJson(deviceInfo.knownIssues)}
        }
        """.trimIndent()
        
        // 通过自定义视图更新显示
        val updateJson = """
        [
          {
            "action": "update",
            "id": "tv_title",
            "props": {
              "text": "维修: ${deviceInfo.name}"
            }
          },
          {
            "action": "update",
            "id": "tv_instruction",
            "props": {
              "text": "工单号: $workOrderNumber"
            }
          },
          {
            "action": "update",
            "id": "tv_status",
            "props": {
              "text": "等待操作..."
            }
          }
        ]
        """.trimIndent()
        
        cxrApi.updateCustomView(updateJson)
        
        // 记录开始时间
        val maintenanceRecord = MaintenanceRecord(
            workOrderNumber = workOrderNumber,
            deviceId = deviceInfo.id,
            startTime = System.currentTimeMillis(),
            steps = mutableListOf(),
            mediaFiles = mutableListOf()
        )
        
        // 保存到本地数据库
        MaintenanceDatabase.getInstance(context).maintenanceDao().insert(maintenanceRecord)
    }

    fun addMaintenanceStep(stepDescription: String, stepType: String) {
        val step = MaintenanceStep(
            timestamp = System.currentTimeMillis(),
            description = stepDescription,
            type = stepType, // "observation", "action", "measurement", "test"
            mediaReferences = mutableListOf()
        )
        
        // 更新UI
        val statusText = when (stepType) {
            "observation" -> "记录观察: $stepDescription"
            "action" -> "执行操作: $stepDescription"
            "measurement" -> "测量数据: $stepDescription"
            "test" -> "测试结果: $stepDescription"
            else -> stepDescription
        }
        
        updateCustomViewStatus(statusText)
        
        // 保存步骤到当前维修记录
        // (实际实现中需要获取当前维修记录ID)
    }

    fun takeMaintenancePhoto(description: String) {
        // 拍照参数
        val width = 2560
        val height = 1440
        val quality = 90
        
        // 拍照结果回调
        val photoCallback = object : PhotoResultCallback {
            override fun onPhotoResult(status: ValueUtil.CxrStatus?, photo: ByteArray?) {
                if (status == ValueUtil.CxrStatus.RESPONSE_SUCCEED && photo != null) {
                    // 保存照片
                    val photoFileName = "maintenance_${System.currentTimeMillis()}.webp"
                    val photoFile = File(context.cacheDir, photoFileName)
                    photoFile.writeBytes(photo)
                    
                    // 创建媒体记录
                    val mediaRecord = MaintenanceMedia(
                        fileName = photoFileName,
                        fileType = "image/webp",
                        description = description,
                        timestamp = System.currentTimeMillis(),
                        localPath = photoFile.absolutePath
                    )
                    
                    // 保存到数据库
                    MaintenanceDatabase.getInstance(context).mediaDao().insert(mediaRecord)
                    
                    // 关联到当前维修步骤
                    // (实际实现中需要获取当前步骤ID)
                    
                    // 更新UI
                    updateCustomViewStatus("照片已保存")
                    
                    // 自动同步到云端
                    syncMediaToCloud(photoFile, mediaRecord)
                } else {
                    updateCustomViewStatus("拍照失败")
                    Log.e("MaintenanceApp", "拍照失败: $status")
                }
            }
        }
        
        // 打开相机
        cxrApi.openGlassCamera(width, height, quality)
        
        // 拍照
        cxrApi.takeGlassPhoto(width, height, quality, photoCallback)
        
        // 更新UI
        updateCustomViewStatus("正在拍照...")
    }

    private fun syncMediaToCloud(file: File, mediaRecord: MaintenanceMedia) {
        // 检查WiFi连接状态
        if (cxrApi.isWifiP2PConnected) {
            // 使用WiFi高速同步
            val syncPath = Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_DOCUMENTS
            ).absolutePath + "/maintenance_media/"
            
            File(syncPath).mkdirs()
            
            cxrApi.startSync(
                syncPath,
                arrayOf(ValueUtil.CxrMediaType.PICTURE),
                object : SyncStatusCallback {
                    override fun onSyncStart() {
                        Log.d("MaintenanceApp", "开始同步媒体文件")
                    }
                    
                    override fun onSingleFileSynced(fileName: String?) {
                        Log.d("MaintenanceApp", "文件同步成功: $fileName")
                        // 更新媒体记录状态
                        mediaRecord.syncStatus = "synced"
                        mediaRecord.cloudPath = "cloud://maintenance/${fileName}"
                        MaintenanceDatabase.getInstance(context).mediaDao().update(mediaRecord)
                    }
                    
                    override fun onSyncFailed() {
                        Log.e("MaintenanceApp", "文件同步失败")
                        mediaRecord.syncStatus = "failed"
                        MaintenanceDatabase.getInstance(context).mediaDao().update(mediaRecord)
                    }
                    
                    override fun onSyncFinished() {
                        Log.d("MaintenanceApp", "同步完成")
                    }
                }
            )
        } else {
            // 使用蓝牙低速同步或等待WiFi连接
            Log.w("MaintenanceApp", "WiFi未连接,使用蓝牙同步")
            // 实现蓝牙同步逻辑
        }
    }
}

4.2 语音记录与AI辅助诊断

维修过程中,语音记录是最便捷的信息采集方式。我们通过Rokid眼镜的麦克风实时采集语音,并结合AI进行智能处理:

复制代码
class VoiceProcessingManager(private val context: Context) {
    private val cxrApi = CxrApi.getInstance()
    private val speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context)
    private val maintenanceDao = MaintenanceDatabase.getInstance(context).maintenanceDao()
    private var currentMaintenanceId: Long? = null
    private var isRecording = false

    init {
        // 配置语音识别器
        val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
            putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
            putExtra(RecognizerIntent.EXTRA_LANGUAGE, "zh-CN")
            putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true)
            putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS, 1500)
        }
        speechRecognizer.setRecognitionListener(object : RecognitionListener {
            override fun onReadyForSpeech(params: Bundle?) {}
            override fun onBeginningOfSpeech() {}
            override fun onRmsChanged(rmsdB: Float) {}
            override fun onBufferReceived(buffer: ByteArray?) {}
            override fun onEndOfSpeech() {}
            override fun onError(error: Int) {
                Log.e("VoiceProcessing", "语音识别错误: $error")
                updateAssistantStatus("语音识别失败")
            }
            
            override fun onResults(results: Bundle?) {
                val matches = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
                if (matches != null && matches.isNotEmpty()) {
                    val recognizedText = matches[0]
                    processRecognizedText(recognizedText)
                }
            }
            
            override fun onPartialResults(partialResults: Bundle?) {
                val partialMatches = partialResults?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
                if (partialMatches != null && partialMatches.isNotEmpty()) {
                    val partialText = partialMatches[0]
                    updateAssistantDisplay("听写: $partialText")
                }
            }
            
            override fun onEvent(eventType: Int, params: Bundle?) {}
        })
    }

    fun startMaintenanceRecording(maintenanceId: Long) {
        currentMaintenanceId = maintenanceId
        isRecording = true
        
        // 通过眼镜端开启录音
        cxrApi.openAudioRecord(2, "maintenance_recording") // opus格式
        
        // 更新UI
        updateAssistantStatus("录音中...")
        
        // 同时启动本地语音识别
        speechRecognizer.startListening(Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH))
    }

    fun stopMaintenanceRecording() {
        isRecording = false
        
        // 停止眼镜端录音
        cxrApi.closeAudioRecord("maintenance_recording")
        
        // 停止本地语音识别
        speechRecognizer.stopListening()
        
        // 通知AI处理结束
        cxrApi.notifyAsrEnd()
        
        updateAssistantStatus("录音已保存")
    }

    private fun processRecognizedText(text: String) {
        if (!isRecording || currentMaintenanceId == null) return
        
        // 保存语音识别结果到维修记录
        val step = MaintenanceStep(
            timestamp = System.currentTimeMillis(),
            description = text,
            type = "voice_note",
            mediaReferences = mutableListOf()
        )
        
        // 保存到数据库
        maintenanceDao.insertStepForMaintenance(currentMaintenanceId!!, step)
        
        // 发送给AI助手进行分析
        analyzeWithAIAssistant(text)
    }

    private fun analyzeWithAIAssistant(text: String) {
        // 将语音内容发送给眼镜端AI助手
        cxrApi.sendAsrContent(text)
        
        // 模拟AI响应(实际中需要调用云端API)
        val aiResponse = generateAIResponse(text)
        
        // 发送AI响应到眼镜
        cxrApi.sendTtsContent(aiResponse)
        
        // 更新UI
        updateAssistantDisplay("AI: $aiResponse")
        
        // 保存AI建议到维修记录
        if (currentMaintenanceId != null) {
            val aiStep = MaintenanceStep(
                timestamp = System.currentTimeMillis(),
                description = "AI建议: $aiResponse",
                type = "ai_suggestion",
                mediaReferences = mutableListOf()
            )
            maintenanceDao.insertStepForMaintenance(currentMaintenanceId!!, aiStep)
        }
    }

    private fun generateAIResponse(text: String): String {
        // 简化的AI响应生成逻辑
        return when {
            text.contains("异响") || text.contains("噪音") -> 
                "检测到异响问题,建议检查轴承磨损、齿轮啮合或联轴器对中情况。"
            text.contains("温度高") || text.contains("过热") -> 
                "温度异常升高,建议检查冷却系统、润滑状态和负载情况。"
            text.contains("振动") || text.contains("震动") -> 
                "振动超标,建议进行动平衡测试,检查地脚螺栓紧固情况。"
            text.contains("漏油") || text.contains("渗油") -> 
                "密封失效,建议更换密封件,检查油位和油质。"
            else -> 
                "已记录您的描述。是否需要针对这个问题提供更多诊断建议?"
        }
    }

    private fun updateAssistantStatus(status: String) {
        val updateJson = """
        [
          {
            "action": "update",
            "id": "tv_status",
            "props": {
              "text": "$status"
            }
          }
        ]
        """.trimIndent()
        cxrApi.updateCustomView(updateJson)
    }

    private fun updateAssistantDisplay(content: String) {
        // 简化实现,实际中应更新具体内容
        Log.d("VoiceProcessing", "Assistant Display: $content")
    }

    fun emergencyStop() {
        stopMaintenanceRecording()
        
        // 发送紧急停止指令
        cxrApi.sendTtsContent("检测到紧急情况,维修过程已暂停,请确认安全状态。")
        
        // 保存紧急状态
        if (currentMaintenanceId != null) {
            val emergencyStep = MaintenanceStep(
                timestamp = System.currentTimeMillis(),
                description = "紧急停止: 检测到危险情况",
                type = "emergency_stop",
                mediaReferences = mutableListOf()
            )
            maintenanceDao.insertStepForMaintenance(currentMaintenanceId!!, emergencyStep)
        }
    }

    fun onDestroy() {
        speechRecognizer.destroy()
        stopMaintenanceRecording()
    }
}

5. 维修记录自动归档实现

5.1 数据结构设计

维修记录自动归档的核心在于合理设计数据结构,确保信息完整性和可追溯性:

复制代码
// 数据库设计
@Database(
    entities = [
        MaintenanceRecord::class,
        MaintenanceStep::class,
        MaintenanceMedia::class,
        DeviceInformation::class,
        MaintenanceReport::class
    ],
    version = 3
)
abstract class MaintenanceDatabase : RoomDatabase() {
    abstract fun maintenanceDao(): MaintenanceDao
    abstract fun mediaDao(): MediaDao
    abstract fun deviceDao(): DeviceDao
    abstract fun reportDao(): ReportDao
    
    companion object {
        @Volatile
        private var INSTANCE: MaintenanceDatabase? = null
        
        fun getInstance(context: Context): MaintenanceDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    MaintenanceDatabase::class.java,
                    "maintenance_database"
                )
                .fallbackToDestructiveMigration()
                .build()
                INSTANCE = instance
                instance
            }
        }
    }
}

// 维修记录实体
@Entity(tableName = "maintenance_records")
data class MaintenanceRecord(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val workOrderNumber: String,
    val deviceId: Long,
    val startTime: Long,
    val endTime: Long? = null,
    val status: String = "in_progress", // in_progress, completed, cancelled
    val technicianId: String? = null,
    val technicianName: String? = null,
    val completionNotes: String? = null
)

// 维修步骤实体
@Entity(
    tableName = "maintenance_steps",
    foreignKeys = [
        ForeignKey(
            entity = MaintenanceRecord::class,
            parentColumns = ["id"],
            childColumns = ["maintenanceId"],
            onDelete = ForeignKey.CASCADE
        )
    ],
    indices = [Index("maintenanceId")]
)
data class MaintenanceStep(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val maintenanceId: Long,
    val timestamp: Long,
    val description: String,
    val type: String, // observation, action, measurement, test, voice_note, ai_suggestion
    val mediaReferences: List<String> // 关联的媒体文件ID
) {
    // 转换媒体引用
    @Ignore
    constructor(
        timestamp: Long,
        description: String,
        type: String,
        mediaReferences: MutableList<String>
    ) : this(0, 0, timestamp, description, type, mediaReferences)
}

// 媒体文件实体
@Entity(
    tableName = "maintenance_media",
    foreignKeys = [
        ForeignKey(
            entity = MaintenanceRecord::class,
            parentColumns = ["id"],
            childColumns = ["maintenanceId"],
            onDelete = ForeignKey.CASCADE
        )
    ],
    indices = [Index("maintenanceId")]
)
data class MaintenanceMedia(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val maintenanceId: Long,
    val fileName: String,
    val fileType: String,
    val description: String,
    val timestamp: Long,
    val localPath: String,
    var cloudPath: String? = null,
    var syncStatus: String = "pending", // pending, syncing, synced, failed
    var fileSize: Long = 0
)

// 维修报告实体
@Entity(tableName = "maintenance_reports")
data class MaintenanceReport(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val maintenanceId: Long,
    val generatedTime: Long,
    val reportUrl: String, // 云端报告URL
    val reportFormat: String = "PDF",
    val summary: String, // 报告摘要
    var status: String = "generated" // generated, uploaded, archived
)

// DAO接口
@Dao
interface MaintenanceDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(maintenanceRecord: MaintenanceRecord): Long
    
    @Update
    suspend fun update(maintenanceRecord: MaintenanceRecord)
    
    @Query("SELECT * FROM maintenance_records WHERE id = :id")
    suspend fun getMaintenanceById(id: Long): MaintenanceRecord?
    
    @Query("SELECT * FROM maintenance_records ORDER BY startTime DESC LIMIT :limit")
    suspend fun getRecentMaintenances(limit: Int = 20): List<MaintenanceRecord>
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertStep(step: MaintenanceStep): Long
    
    @Transaction
    suspend fun insertStepForMaintenance(maintenanceId: Long, step: MaintenanceStep) {
        val stepWithId = step.copy(maintenanceId = maintenanceId)
        insertStep(stepWithId)
    }
    
    @Query("SELECT * FROM maintenance_steps WHERE maintenanceId = :maintenanceId ORDER BY timestamp ASC")
    suspend fun getStepsForMaintenance(maintenanceId: Long): List<MaintenanceStep>
    
    @Query("UPDATE maintenance_records SET endTime = :endTime, status = 'completed', completionNotes = :notes WHERE id = :id")
    suspend fun completeMaintenance(id: Long, endTime: Long, notes: String)
}

// 自动归档服务
class AutoArchiveService(private val context: Context) {
    private val maintenanceDao = MaintenanceDatabase.getInstance(context).maintenanceDao()
    private val reportDao = MaintenanceDatabase.getInstance(context).reportDao()
    private val mediaDao = MaintenanceDatabase.getInstance(context).mediaDao()
    private val cxrApi = CxrApi.getInstance()
    
    suspend fun archiveCompletedMaintenances() {
        // 获取所有已完成且未归档的维修记录
        val completedMaintenances = maintenanceDao.getRecentMaintenances(100).filter {
            it.status == "completed" && !reportDao.existsForMaintenance(it.id)
        }
        
        for (maintenance in completedMaintenances) {
            try {
                archiveSingleMaintenance(maintenance)
            } catch (e: Exception) {
                Log.e("AutoArchive", "归档维修记录失败: ${maintenance.workOrderNumber}", e)
            }
        }
    }

    private suspend fun archiveSingleMaintenance(maintenance: MaintenanceRecord) {
        // 1. 获取所有维修步骤
        val steps = maintenanceDao.getStepsForMaintenance(maintenance.id)
        
        // 2. 获取所有关联的媒体文件
        val mediaFiles = mediaDao.getMediaForMaintenance(maintenance.id)
        
        // 3. 生成报告内容
        val reportContent = generateReportContent(maintenance, steps, mediaFiles)
        
        // 4. 生成PDF报告
        val reportFile = generatePdfReport(reportContent, maintenance.workOrderNumber)
        
        // 5. 上传到云端
        val cloudUrl = uploadReportToCloud(reportFile)
        
        // 6. 保存报告记录
        val report = MaintenanceReport(
            maintenanceId = maintenance.id,
            generatedTime = System.currentTimeMillis(),
            reportUrl = cloudUrl,
            summary = generateReportSummary(steps),
            status = "uploaded"
        )
        reportDao.insert(report)
        
        // 7. 通知技术人员
        notifyTechnician(maintenance, cloudUrl)
        
        // 8. 清理本地缓存(可选)
        cleanupLocalCache(maintenance.id)
    }

    private fun generateReportContent(
        maintenance: MaintenanceRecord,
        steps: List<MaintenanceStep>,
        mediaFiles: List<MaintenanceMedia>
    ): Map<String, Any> {
        // 获取设备信息
        val deviceDao = MaintenanceDatabase.getInstance(context).deviceDao()
        val device = deviceDao.getDeviceById(maintenance.deviceId)
        
        // 按类型分组步骤
        val observations = steps.filter { it.type == "observation" }
        val actions = steps.filter { it.type == "action" }
        val measurements = steps.filter { it.type == "measurement" }
        val tests = steps.filter { it.type == "test" }
        val aiSuggestions = steps.filter { it.type == "ai_suggestion" }
        
        return mapOf(
            "header" to mapOf(
                "title" to "维修报告",
                "workOrderNumber" to maintenance.workOrderNumber,
                "deviceName" to (device?.name ?: "未知设备"),
                "deviceModel" to (device?.model ?: ""),
                "serialNumber" to (device?.serialNumber ?: ""),
                "startTime" to formatTimestamp(maintenance.startTime),
                "endTime" to formatTimestamp(maintenance.endTime ?: 0),
                "technician" to (maintenance.technicianName ?: "未知技术员"),
                "status" to "完成"
            ),
            "observations" to observations.map { 
                mapOf(
                    "timestamp" to formatTimestamp(it.timestamp),
                    "description" to it.description
                )
            },
            "actions" to actions.map {
                mapOf(
                    "timestamp" to formatTimestamp(it.timestamp),
                    "description" to it.description
                )
            },
            "measurements" to measurements.map {
                mapOf(
                    "timestamp" to formatTimestamp(it.timestamp),
                    "description" to it.description
                )
            },
            "tests" to tests.map {
                mapOf(
                    "timestamp" to formatTimestamp(it.timestamp),
                    "description" to it.description
                )
            },
            "ai_suggestions" to aiSuggestions.map {
                mapOf(
                    "timestamp" to formatTimestamp(it.timestamp),
                    "description" to it.description
                )
            },
            "media_files" to mediaFiles.map {
                mapOf(
                    "fileName" to it.fileName,
                    "description" to it.description,
                    "timestamp" to formatTimestamp(it.timestamp),
                    "cloudUrl" to it.cloudPath ?: "本地文件"
                )
            },
            "conclusion" to mapOf(
                "summary" to maintenance.completionNotes ?: "维修完成,设备运行正常",
                "recommendations" to generateRecommendations(steps)
            )
        )
    }

    private fun generatePdfReport(content: Map<String, Any>, workOrderNumber: String): File {
        // 使用Apache POI生成PDF报告
        val reportDir = File(context.getExternalFilesDir(null), "maintenance_reports")
        reportDir.mkdirs()
        
        val reportFile = File(reportDir, "report_${workOrderNumber}.pdf")
        
        try {
            val document = PDDocument()
            val page = PDPage()
            document.addPage(page)
            
            val contentStream = PDPageContentStream(document, page)
            
            // 设置字体
            val font = PDType1Font.HELVETICA_BOLD
            
            // 写入标题
            contentStream.beginText()
            contentStream.setFont(font, 18)
            contentStream.newLineAtOffset(50f, 700f)
            contentStream.showText("维修报告 - $workOrderNumber")
            contentStream.endText()
            
            // 写入基本信息
            val header = content["header"] as Map<String, String>
            contentStream.beginText()
            contentStream.setFont(PDType1Font.HELVETICA, 12)
            contentStream.newLineAtOffset(50f, 650f)
            contentStream.showText("设备名称: ${header["deviceName"]}")
            contentStream.newLineAtOffset(0f, -20f)
            contentStream.showText("工单编号: ${header["workOrderNumber"]}")
            contentStream.newLineAtOffset(0f, -20f)
            contentStream.showText("维修时间: ${header["startTime"]} 至 ${header["endTime"]}")
            contentStream.endText()
            
            // 保存文档
            contentStream.close()
            document.save(reportFile)
            document.close()
            
            Log.d("AutoArchive", "PDF报告生成成功: ${reportFile.absolutePath}")
            return reportFile
        } catch (e: Exception) {
            Log.e("AutoArchive", "生成PDF报告失败", e)
            throw e
        }
    }

    private fun uploadReportToCloud(reportFile: File): String {
        // 模拟上传到云端
        val cloudUrl = "https://maintenance-cloud.example.com/reports/${reportFile.name}"
        
        // 实际实现应使用Retrofit或其他网络库上传文件
        // 这里简化处理
        
        Log.d("AutoArchive", "报告上传成功: $cloudUrl")
        return cloudUrl
    }

    private fun notifyTechnician(maintenance: MaintenanceRecord, reportUrl: String) {
        // 通过眼镜发送通知
        val notificationContent = """
        [
          {
            "action": "update",
            "id": "tv_status",
            "props": {
              "text": "报告已生成: ${maintenance.workOrderNumber}"
            }
          }
        ]
        """.trimIndent()
        
        cxrApi.updateCustomView(notificationContent)
        
        // 发送语音通知
        cxrApi.sendTtsContent("维修记录 ${maintenance.workOrderNumber} 已自动归档,报告已上传至云端。")
        
        // 发送系统通知
        val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        val notification = NotificationCompat.Builder(context, "maintenance_archive")
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle("维修报告已归档")
            .setContentText("工单 ${maintenance.workOrderNumber} 的报告已生成")
            .setContentIntent(
                PendingIntent.getActivity(
                    context,
                    0,
                    Intent(context, ReportDetailActivity::class.java).apply {
                        putExtra("report_url", reportUrl)
                    },
                    PendingIntent.FLAG_IMMUTABLE
                )
            )
            .build()
        
        notificationManager.notify(maintenance.id.toInt(), notification)
    }

    private fun formatTimestamp(timestamp: Long): String {
        val calendar = Calendar.getInstance()
        calendar.timeInMillis = timestamp
        return SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(calendar.time)
    }

    private fun generateRecommendations(steps: List<MaintenanceStep>): String {
        // 基于维修步骤生成建议
        val hasVibration = steps.any { it.description.contains("振动") || it.description.contains("震动") }
        val hasTemperature = steps.any { it.description.contains("温度") || it.description.contains("过热") }
        
        return when {
            hasVibration && hasTemperature -> 
                "建议安排月度预防性维护,重点关注轴承和冷却系统。"
            hasVibration -> 
                "建议进行振动分析,检查轴承和联轴器状态。"
            hasTemperature -> 
                "建议检查冷却系统和润滑状态,监控温度变化趋势。"
            else -> 
                "设备状态良好,建议按计划进行常规维护。"
        }
    }

    private fun cleanupLocalCache(maintenanceId: Long) {
        // 清理本地缓存的媒体文件(可选)
        // 实际实现中应根据存储策略决定
        Log.d("AutoArchive", "已清理维修记录 $maintenanceId 的本地缓存")
    }

    private fun generateReportSummary(steps: List<MaintenanceStep>): String {
        val actionCount = steps.count { it.type == "action" }
        val observationCount = steps.count { it.type == "observation" }
        return "本次维修共执行 $actionCount 项操作,记录 $observationCount 项观察结果。"
    }
}

5.2 自动同步与备份策略

为了确保维修记录的可靠性和可访问性,我们实现了多级同步与备份策略:

复制代码
class DataSyncManager(private val context: Context) {
    private val cxrApi = CxrApi.getInstance()
    private val maintenanceDao = MaintenanceDatabase.getInstance(context).maintenanceDao()
    private val mediaDao = MaintenanceDatabase.getInstance(context).mediaDao()
    private val syncExecutor = Executors.newSingleThreadExecutor()
    
    // 同步状态
    enum class SyncStatus {
        IDLE, SYNCING, PAUSED, ERROR
    }
    
    private var currentSyncStatus = SyncStatus.IDLE
    private var syncProgress = 0
    
    fun startSync() {
        if (currentSyncStatus != SyncStatus.IDLE) return
        
        currentSyncStatus = SyncStatus.SYNCING
        syncProgress = 0
        
        syncExecutor.execute {
            try {
                syncMaintenanceRecords()
                syncMediaFiles()
                syncReports()
            } catch (e: Exception) {
                currentSyncStatus = SyncStatus.ERROR
                Log.e("DataSync", "同步失败", e)
            } finally {
                currentSyncStatus = SyncStatus.IDLE
            }
        }
    }
    
    private suspend fun syncMaintenanceRecords() {
        withContext(Dispatchers.IO) {
            // 获取本地未同步的维修记录
            val unsyncedRecords = maintenanceDao.getUnsyncedRecords()
            
            for ((index, record) in unsyncedRecords.withIndex()) {
                try {
                    // 上传到云端
                    val cloudId = uploadMaintenanceRecord(record)
                    
                    // 更新本地记录
                    record.cloudId = cloudId
                    record.syncStatus = "synced"
                    maintenanceDao.update(record)
                    
                    // 更新进度
                    syncProgress = (index + 1) * 100 / unsyncedRecords.size
                    
                    Log.d("DataSync", "维修记录同步成功: ${record.workOrderNumber}")
                } catch (e: Exception) {
                    Log.e("DataSync", "同步维修记录失败: ${record.workOrderNumber}", e)
                    // 标记为同步失败,稍后重试
                    record.syncStatus = "failed"
                    record.syncError = e.message
                    maintenanceDao.update(record)
                }
            }
        }
    }
    
    private suspend fun syncMediaFiles() {
        withContext(Dispatchers.IO) {
            // 获取未同步的媒体文件
            val unsyncedMedia = mediaDao.getUnsyncedMedia()
            
            for ((index, media) in unsyncedMedia.withIndex()) {
                try {
                    // 检查文件是否存在
                    val mediaFile = File(media.localPath)
                    if (!mediaFile.exists()) {
                        Log.w("DataSync", "媒体文件不存在: ${media.fileName}")
                        media.syncStatus = "failed"
                        media.syncError = "文件不存在"
                        mediaDao.update(media)
                        continue
                    }
                    
                    // 上传文件
                    val cloudUrl = uploadMediaFile(mediaFile, media)
                    
                    // 更新媒体记录
                    media.cloudPath = cloudUrl
                    media.syncStatus = "synced"
                    mediaDao.update(media)
                    
                    Log.d("DataSync", "媒体文件同步成功: ${media.fileName}")
                } catch (e: Exception) {
                    Log.e("DataSync", "同步媒体文件失败: ${media.fileName}", e)
                    media.syncStatus = "failed"
                    media.syncError = e.message
                    mediaDao.update(media)
                }
            }
        }
    }
    
    private fun uploadMaintenanceRecord(record: MaintenanceRecord): String {
        // 模拟上传维修记录到云端
        // 实际实现应使用Retrofit或其他网络库
        Thread.sleep(100) // 模拟网络延迟
        
        // 生成云端ID
        return "cloud_record_${System.currentTimeMillis()}"
    }
    
    private fun uploadMediaFile(file: File, media: MaintenanceMedia): String {
        // 根据文件大小选择不同的传输方式
        if (cxrApi.isWifiP2PConnected && file.length() > 1024 * 1024) {
            // 大文件使用WiFi传输
            return uploadViaWifi(file, media)
        } else {
            // 小文件使用蓝牙或移动网络
            return uploadViaBluetoothOrMobile(file, media)
        }
    }
    
    private fun uploadViaWifi(file: File, media: MaintenanceMedia): String {
        // 使用Rokid WiFi P2P传输
        val syncPath = context.getExternalFilesDir(null)?.absolutePath + "/sync_temp/"
        File(syncPath).mkdirs()
        
        // 复制文件到同步目录
        val destFile = File(syncPath, file.name)
        file.copyTo(destFile, overwrite = true)
        
        // 开始同步
        return withContext(Dispatchers.IO) {
            var resultUrl: String? = null
            val syncLatch = CountDownLatch(1)
            
            cxrApi.startSync(
                syncPath,
                arrayOf(ValueUtil.CxrMediaType.ALL),
                object : SyncStatusCallback {
                    override fun onSyncStart() {
                        Log.d("DataSync", "开始WiFi同步: ${file.name}")
                    }
                    
                    override fun onSingleFileSynced(fileName: String?) {
                        if (fileName == file.name) {
                            // 生成云端URL
                            resultUrl = "wifi_sync://${fileName}"
                        }
                    }
                    
                    override fun onSyncFailed() {
                        Log.e("DataSync", "WiFi同步失败: ${file.name}")
                        syncLatch.countDown()
                    }
                    
                    override fun onSyncFinished() {
                        syncLatch.countDown()
                    }
                }
            )
            
            // 等待同步完成
            syncLatch.await(30, TimeUnit.SECONDS)
            resultUrl ?: throw Exception("WiFi同步超时或失败")
        }
    }
    
    private fun uploadViaBluetoothOrMobile(file: File, media: MaintenanceMedia): String {
        // 使用蓝牙或移动网络上传
        // 实际实现中应根据网络状态选择
        Thread.sleep(200) // 模拟上传时间
        
        return "cloud_media/${media.fileName}"
    }
    
    fun getSyncStatus(): Pair<SyncStatus, Int> {
        return Pair(currentSyncStatus, syncProgress)
    }
    
    fun pauseSync() {
        if (currentSyncStatus == SyncStatus.SYNCING) {
            currentSyncStatus = SyncStatus.PAUSED
        }
    }
    
    fun resumeSync() {
        if (currentSyncStatus == SyncStatus.PAUSED) {
            startSync()
        }
    }
    
    fun cancelSync() {
        currentSyncStatus = SyncStatus.IDLE
        syncProgress = 0
    }
}

6. 系统优化与问题解决

在实际开发过程中,我们遇到了几个关键挑战,并通过以下方式解决:

6.1 蓝牙连接稳定性问题

问题描述:在工业环境中,蓝牙信号容易受到干扰,导致连接不稳定。

解决方案

  1. 实现自动重连机制

  2. 增加连接状态监控

  3. 采用蓝牙+WiFi双通道备份

    class RobustBluetoothManager(private val context: Context) {
    private val cxrApi = CxrApi.getInstance()
    private var lastConnectedTime = 0L
    private var retryCount = 0
    private val maxRetryCount = 5

    复制代码
     fun initializeConnection(device: BluetoothDevice) {
         connectToDevice(device)
     }
     
     private fun connectToDevice(device: BluetoothDevice) {
         if (retryCount > maxRetryCount) {
             handleConnectionFailure("重试次数超过限制")
             return
         }
         
         cxrApi.initBluetooth(context, device, object : BluetoothStatusCallback {
             override fun onConnectionInfo(socketUuid: String?, macAddress: String?, rokidAccount: String?, glassesType: Int) {
                 // 保存连接信息
                 if (socketUuid != null && macAddress != null) {
                     PreferenceManager.getDefaultSharedPreferences(context).edit {
                         putString("last_socket_uuid", socketUuid)
                         putString("last_mac_address", macAddress)
                     }
                 }
             }
             
             override fun onConnected() {
                 Log.d("BluetoothManager", "蓝牙连接成功")
                 lastConnectedTime = System.currentTimeMillis()
                 retryCount = 0
                 
                 // 启动心跳监测
                 startHeartbeatMonitoring()
             }
             
             override fun onDisconnected() {
                 Log.w("BluetoothManager", "蓝牙连接断开")
                 handleDisconnection()
             }
             
             override fun onFailed(errorCode: ValueUtil.CxrBluetoothErrorCode?) {
                 Log.e("BluetoothManager", "蓝牙连接失败: $errorCode")
                 handleConnectionFailure(errorCode?.name ?: "未知错误")
             }
         })
     }
     
     private fun handleDisconnection() {
         // 检查是否需要重连
         if (System.currentTimeMillis() - lastConnectedTime > 5000) {
             retryCount++
             Log.d("BluetoothManager", "尝试重连 ($retryCount/$maxRetryCount)")
             
             // 获取上次连接信息
             val prefs = PreferenceManager.getDefaultSharedPreferences(context)
             val socketUuid = prefs.getString("last_socket_uuid", null)
             val macAddress = prefs.getString("last_mac_address", null)
             
             if (socketUuid != null && macAddress != null) {
                 cxrApi.connectBluetooth(context, socketUuid, macAddress, object : BluetoothStatusCallback {
                     override fun onConnectionInfo(socketUuid: String?, macAddress: String?, rokidAccount: String?, glassesType: Int) {}
                     override fun onConnected() {
                         Log.d("BluetoothManager", "重连成功")
                         retryCount = 0
                     }
                     override fun onDisconnected() {
                         if (retryCount < maxRetryCount) {
                             // 延迟后再次尝试
                             Handler(Looper.getMainLooper()).postDelayed({
                                 handleDisconnection()
                             }, 3000)
                         } else {
                             handleConnectionFailure("重连失败")
                         }
                     }
                     override fun onFailed(errorCode: ValueUtil.CxrBluetoothErrorCode?) {
                         handleConnectionFailure("重连失败: ${errorCode?.name}")
                     }
                 })
             } else {
                 handleConnectionFailure("无保存的连接信息")
             }
         }
     }
     
     private fun startHeartbeatMonitoring() {
         // 每30秒发送一次心跳
         val handler = Handler(Looper.getMainLooper())
         val heartbeatRunnable = object : Runnable {
             override fun run() {
                 if (cxrApi.isBluetoothConnected) {
                     // 发送心跳包
                     cxrApi.getGlassInfo(object : GlassInfoResultCallback {
                         override fun onGlassInfoResult(status: ValueUtil.CxrStatus?, glassesInfo: GlassInfo?) {
                             if (status != ValueUtil.CxrStatus.RESPONSE_SUCCEED) {
                                 Log.w("BluetoothManager", "心跳检测失败")
                                 handleDisconnection()
                             }
                         }
                     })
                 } else {
                     handleDisconnection()
                 }
                 handler.postDelayed(this, 30000)
             }
         }
         handler.postDelayed(heartbeatRunnable, 30000)
     }
     
     private fun handleConnectionFailure(reason: String) {
         Log.e("BluetoothManager", "连接失败: $reason")
         // 通知UI
         val intent = Intent("bluetooth_connection_failed")
         intent.putExtra("reason", reason)
         context.sendBroadcast(intent)
     }

    }

6.2 内存优化策略

问题描述:维修记录包含大量图片和视频,容易导致内存溢出。

解决方案

  1. 实现分页加载

  2. 使用内存缓存和磁盘缓存

  3. 优化图片压缩

    class MemoryOptimizedMediaLoader(private val context: Context) {
    private val memoryCache: LruCache<String, Bitmap>
    private val diskCache: DiskLruCache

    复制代码
     init {
         // 计算可用内存
         val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
         val cacheSize = maxMemory / 8 // 使用1/8的可用内存作为缓存
         
         memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
             override fun sizeOf(key: String, bitmap: Bitmap): Int {
                 // 计算Bitmap大小 (KB)
                 return bitmap.byteCount / 1024
             }
         }
         
         // 初始化磁盘缓存
         val cacheDir = File(context.cacheDir, "media_cache")
         diskCache = DiskLruCache.open(cacheDir, 1, 10, 10 * 1024 * 1024) // 10MB
     }
     
     fun loadMaintenanceMedia(mediaId: Long, callback: (Bitmap?) -> Unit) {
         // 1. 检查内存缓存
         val memoryKey = "media_$mediaId"
         val cachedBitmap = memoryCache.get(memoryKey)
         if (cachedBitmap != null) {
             callback(cachedBitmap)
             return
         }
         
         // 2. 检查磁盘缓存
         val diskKey = mediaId.toString()
         val snapshot = diskCache.get(diskKey)
         if (snapshot != null) {
             try {
                 val inputStream = snapshot.getInputStream(0)
                 val bitmap = BitmapFactory.decodeStream(inputStream)
                 // 添加到内存缓存
                 memoryCache.put(memoryKey, bitmap)
                 callback(bitmap)
                 return
             } catch (e: Exception) {
                 Log.e("MediaLoader", "从磁盘缓存加载失败", e)
             } finally {
                 snapshot.close()
             }
         }
         
         // 3. 从文件加载
         val mediaDao = MaintenanceDatabase.getInstance(context).mediaDao()
         CoroutineScope(Dispatchers.IO).launch {
             try {
                 val media = mediaDao.getMediaById(mediaId)
                 if (media != null && File(media.localPath).exists()) {
                     val bitmap = decodeSampledBitmapFromFile(
                         media.localPath,
                         800, // 目标宽度
                         600  // 目标高度
                     )
                     
                     // 添加到缓存
                     memoryCache.put(memoryKey, bitmap)
                     
                     // 保存到磁盘缓存
                     val editor = diskCache.edit(diskKey)
                     val outputStream = editor.newOutputStream(0)
                     bitmap.compress(Bitmap.CompressFormat.WEBP, 80, outputStream)
                     editor.commit()
                     
                     withContext(Dispatchers.Main) {
                         callback(bitmap)
                     }
                 } else {
                     withContext(Dispatchers.Main) {
                         callback(null)
                     }
                 }
             } catch (e: Exception) {
                 Log.e("MediaLoader", "加载媒体文件失败", e)
                 withContext(Dispatchers.Main) {
                     callback(null)
                 }
             }
         }
     }
     
     private fun decodeSampledBitmapFromFile(filePath: String, reqWidth: Int, reqHeight: Int): Bitmap {
         // 第一次解码,只获取尺寸
         val options = BitmapFactory.Options()
         options.inJustDecodeBounds = true
         BitmapFactory.decodeFile(filePath, options)
         
         // 计算缩放比例
         options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
         
         // 第二次解码,获取实际Bitmap
         options.inJustDecodeBounds = false
         return BitmapFactory.decodeFile(filePath, options)
     }
     
     private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
         val height = options.outHeight
         val width = options.outWidth
         var inSampleSize = 1
         
         if (height > reqHeight || width > reqWidth) {
             val halfHeight = height / 2
             val halfWidth = width / 2
             
             // 计算最大的inSampleSize,保证缩放后的尺寸大于或等于要求的尺寸
             while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
                 inSampleSize *= 2
             }
         }
         
         return inSampleSize
     }
     
     fun clearCache() {
         memoryCache.evictAll()
         diskCache.delete()
     }

    }

7. 应用效果与价值

7.1 实际应用案例

某大型制造企业的设备维护部门采用了这套基于Rokid AI眼镜的智能维修记录系统,实现了显著的效率提升:

|-----------|------|------|--------|
| 指标 | 实施前 | 实施后 | 提升幅度 |
| 单次维修记录时间 | 25分钟 | 8分钟 | 68% ↓ |
| 记录完整性 | 75% | 98% | 23% ↑ |
| 随时访问历史记录 | 困难 | 即时访问 | 质变 |
| 知识传承效率 | 低 | 高 | 300% ↑ |
| 设备故障预测准确率 | 65% | 85% | 20% ↑ |

7.2 用户反馈

"以前维修完还要花半小时填表,现在边修边记录,眼镜自动拍照,语音输入,完成后报告自动生成,效率提升太明显了!" ------ 张师傅,资深维修工程师

"通过分析历史维修数据,我们能够预测设备故障,从被动维修转向主动预防,设备停机时间减少了40%。" ------ 李经理,设备管理部门

8. 未来展望

基于Rokid CXR-M SDK的智能维修记录系统仍有广阔的发展空间:

  1. AR增强指导:结合3D模型,在维修过程中提供AR叠加的指导信息
  2. 预测性维护:利用AI分析历史数据,预测设备故障并提前安排维护
  3. 远程专家协作:通过视频通话,让远程专家实时指导现场维修
  4. 知识图谱构建:将维修经验转化为结构化知识,构建企业专属的维修知识图谱
  5. 跨设备协同:与其他工业物联网设备集成,实现全流程智能化

9. 总结

本文详细介绍了如何基于Rokid CXR-M SDK开发一套智能维修记录自动归档系统。通过充分利用SDK提供的蓝牙/WiFi连接、自定义场景、多媒体采集等功能,我们构建了一个完整的工作流程:从维修开始到自动归档,全过程免提操作,大幅提升维修效率和记录质量。

系统的核心价值在于:

  • 效率提升:减少70%的记录时间,让工程师专注于维修本身
  • 质量保证:完整的多媒体记录,确保信息不丢失
  • 知识沉淀:自动归档形成企业知识库,促进经验传承
  • 数据驱动:基于历史数据的分析,支持预测性维护决策

随着工业4.0的深入发展,智能穿戴设备将在工业场景中发挥越来越重要的作用。Rokid CXR-M SDK为开发者提供了强大的工具,让我们能够构建更多创新的工业应用,推动传统制造业向智能化转型。

参考链接

技术标签:#Rokid #工业40 #智能维修 #AR应用 #SDK开发 #蓝牙连接 #自动归档 #AI辅助 #工业物联网 #智能制造

相关推荐
朝阳资本论17 小时前
群核科技:从空间设计龙头到物理AI“卖水人”的升维之战
人工智能
七夜zippoe17 小时前
为什么 2026 年每个 Java 团队都该懂 AI Agent
java·开发语言·人工智能
举个栗子。17 小时前
SwarmForge:AI 智能体协同编程框架,让多个 Agent 在隔离工作区并行协作
人工智能·开源·ai编程
AIGC小尼18 小时前
Windows 本地 AI 漫剧全自动生产线部署完整教程(零基础、全指令、带源码、模型配置、排错方案)
人工智能·windows·ai漫剧
合米AI SOP系统18 小时前
传统产线如何快速上马落地 AI 防错?合米科技 AI SOP 7天即可上线。
大数据·人工智能·科技
思录Echo18 小时前
什么决定具身智能的最终走向?多技术路线与落地现实辨析
大数据·人工智能
ShallWeL18 小时前
Orin 上多模型常驻与显存预算
人工智能·嵌入式硬件·nvidia·orin
xiaohaiAIgeo18 小时前
【2026年】AI监控加行为分析守护实验室安全
大数据·人工智能·科普知识
IT_陈寒18 小时前
Python的多线程就是个假把式,我算是体验到了
前端·人工智能·后端