摘要
本文详细阐述了如何利用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 蓝牙连接稳定性问题
问题描述:在工业环境中,蓝牙信号容易受到干扰,导致连接不稳定。
解决方案:
-
实现自动重连机制
-
增加连接状态监控
-
采用蓝牙+WiFi双通道备份
class RobustBluetoothManager(private val context: Context) {
private val cxrApi = CxrApi.getInstance()
private var lastConnectedTime = 0L
private var retryCount = 0
private val maxRetryCount = 5fun 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 内存优化策略
问题描述:维修记录包含大量图片和视频,容易导致内存溢出。
解决方案:
-
实现分页加载
-
使用内存缓存和磁盘缓存
-
优化图片压缩
class MemoryOptimizedMediaLoader(private val context: Context) {
private val memoryCache: LruCache<String, Bitmap>
private val diskCache: DiskLruCacheinit { // 计算可用内存 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的智能维修记录系统仍有广阔的发展空间:
- AR增强指导:结合3D模型,在维修过程中提供AR叠加的指导信息
- 预测性维护:利用AI分析历史数据,预测设备故障并提前安排维护
- 远程专家协作:通过视频通话,让远程专家实时指导现场维修
- 知识图谱构建:将维修经验转化为结构化知识,构建企业专属的维修知识图谱
- 跨设备协同:与其他工业物联网设备集成,实现全流程智能化
9. 总结
本文详细介绍了如何基于Rokid CXR-M SDK开发一套智能维修记录自动归档系统。通过充分利用SDK提供的蓝牙/WiFi连接、自定义场景、多媒体采集等功能,我们构建了一个完整的工作流程:从维修开始到自动归档,全过程免提操作,大幅提升维修效率和记录质量。
系统的核心价值在于:
- 效率提升:减少70%的记录时间,让工程师专注于维修本身
- 质量保证:完整的多媒体记录,确保信息不丢失
- 知识沉淀:自动归档形成企业知识库,促进经验传承
- 数据驱动:基于历史数据的分析,支持预测性维护决策
随着工业4.0的深入发展,智能穿戴设备将在工业场景中发挥越来越重要的作用。Rokid CXR-M SDK为开发者提供了强大的工具,让我们能够构建更多创新的工业应用,推动传统制造业向智能化转型。
参考链接:
技术标签:#Rokid #工业40 #智能维修 #AR应用 #SDK开发 #蓝牙连接 #自动归档 #AI辅助 #工业物联网 #智能制造
