Ver Fonte

1. 蓝牙功能优化 支持提醒开启蓝牙开关,支持申请位置权限

bjb há 3 meses atrás
pai
commit
aca63d658a

+ 10 - 0
app/src/main/AndroidManifest.xml

@@ -12,6 +12,16 @@
     <!--  通知权限  -->
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
 
+    <!--  蓝牙相关权限  -->
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <!--  Android11及以后,发现蓝牙设备需要这个定位权限  -->
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+
 
     <application
         android:name=".Entry"

+ 95 - 10
app/src/main/java/com/iscs/bozzys/ui/pages/home/HomeCompose.kt

@@ -1,5 +1,6 @@
 package com.iscs.bozzys.ui.pages.home
 
+import android.app.Application
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -46,16 +47,34 @@ import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.zIndex
+import androidx.lifecycle.lifecycleScope
 import coil.compose.AsyncImage
 import com.iscs.bozzys.R
 import com.iscs.bozzys.api.Message
+import com.iscs.bozzys.ui.common.PageBase
 import com.iscs.bozzys.ui.pages.compose.CardBox
 import com.iscs.bozzys.ui.pages.compose.TaskListItem
 import com.iscs.bozzys.ui.pages.message.openPageMessage
 import com.iscs.bozzys.ui.pages.vm.VMHome
 import com.iscs.bozzys.ui.theme.Text
 import com.iscs.bozzys.utils.DateUtil.getShowDateOrTime
+import com.iscs.bozzys.utils.LogUtil
+import com.iscs.bozzys.utils.ble.BleFrameExt
+import com.iscs.bozzys.utils.ble.BleFrameExt.buildBLEGetPowerCMD
+import com.iscs.bozzys.utils.ble.BleFrameExt.buildBLEGetStatusCMD
+import com.iscs.bozzys.utils.ble.BleFrameExt.buildBLESwitchRunModeCMD
+import com.iscs.bozzys.utils.ble.BleFrameExt.buildBLETicketDataCMDList
+import com.iscs.bozzys.utils.ble.BleFrameExt.getPower
+import com.iscs.bozzys.utils.ble.BleFrameExt.getRunMode
+import com.iscs.bozzys.utils.ble.BleFrameExt.getSendTicketResult
+import com.iscs.bozzys.utils.ble.BleFrameExt.getSwitchRunModeResult
+import com.iscs.bozzys.utils.ble.BleFrameExt.getToken
+import com.iscs.bozzys.utils.ble.BleManager
+import com.iscs.bozzys.utils.ble.BleRunMode
+import com.iscs.bozzys.utils.byteArrayToHexString
 import com.iscs.bozzys.utils.getRoleName
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
@@ -150,18 +169,84 @@ private fun TopToolBar(pv: PaddingValues, vm: VMHome) {
                     lineHeight = 10.sp
                 )
             }
-//            Icon(
-//                painter = painterResource(R.drawable.settings),
-//                contentDescription = null,
-//                modifier = Modifier
-//                    .size(36.dp)
-//                    .clip(RoundedCornerShape(6.dp))
-//                    .clickable(onClick = { vmHome.navigationToId(3) })
-//                    .padding(10.dp),
-//                tint = Color.White
-//            )
+            Icon(
+                painter = painterResource(R.drawable.settings),
+                contentDescription = null,
+                modifier = Modifier
+                    .size(36.dp)
+                    .clip(RoundedCornerShape(6.dp))
+                    .clickable(onClick = {
+                        if (ctx is PageBase) {
+                            ctx.lifecycleScope.launch(Dispatchers.IO) {
+                                bleKeyTest(ctx.application, "CC:BA:97:21:72:C6")
+                                // bleKeyTest(ctx.application, "CC:BA:97:21:71:E6")
+                                // bleKeyTest(ctx.application, "CC:BA:97:21:72:0A")
+                                // bleKeyTest(ctx.application, "CC:BA:97:21:71:CA")
+                            }
+                        }
+                    })
+                    .padding(10.dp),
+                tint = Color.White
+            )
+        }
+    }
+}
+
+val testJobJson =
+    "{\"cardNo\":\"D2931A25\",\"data\":[{\"codeId\":1,\"dataList\":[{\"dataId\":87,\"equipName\":\"E_29\",\"equipRfidNo\":\"1B9105AF\",\"target\":0},{\"dataId\":88,\"equipName\":\"E_30\",\"equipRfidNo\":\"FB9091E5\",\"target\":0}],\"taskCode\":\"165\"}],\"lockList\":[{\"lockId\":1,\"rfid\":\"C097D395\"},{\"lockId\":2,\"rfid\":\"A04AD495\"}],\"password\":\"123456\"}"
+
+
+private suspend fun bleKeyTest(application: Application, mac: String) {
+    // 测试蓝牙扫描
+    val bm = BleManager(application, mac = mac)
+    val result = bm.connect()
+    if (result.connected) {
+        // 获取设备token
+        val token = bm.writeByResponse(BleFrameExt.buildBLEGetTokenCMD()).getToken()
+        LogUtil.i("xiaoming $mac", "获取设备token ${token.byteArrayToHexString()}")
+        // 获取设备电量
+        val power = bm.writeByResponse(token.buildBLEGetPowerCMD()).getPower()
+        LogUtil.i("xiaoming $mac", "当前设备电量:$power")
+        // 获取当前设备运行模式
+        val runMode = bm.writeByResponse(token.buildBLEGetStatusCMD()).getRunMode()
+        LogUtil.i("xiaoming $mac", "当前工作模式:$runMode")
+        // 切换当前设备运行模式
+//                val switch = bm.writeByResponse(token.buildBLESwitchRunModeCMD(RunMode.STBY)).getSwitchRunModeResult()
+//                LogUtil.i("xiaoming $mac", "切换工作模式:$switch")
+        // 下发作业票
+        val tickets = token.buildBLETicketDataCMDList(testJobJson)
+        var ticketSendOk = true
+        tickets.forEach {
+            val ticket = bm.writeByResponse(it).getSendTicketResult()
+            LogUtil.i("xiaoming $mac", "下发作业票:分包${it.data[4].toInt()}发送结果:$ticket")
+            if (ticket != 0) {
+                ticketSendOk = false
+                return@forEach
+            }
         }
+        LogUtil.i("xiaoming $mac", "下发作业票:$ticketSendOk")
+        // 作业票下发成功,修改设备运行模式为工作模式
+        val switch = bm.writeByResponse(token.buildBLESwitchRunModeCMD(BleRunMode.WORK)).getSwitchRunModeResult()
+        LogUtil.i("xiaoming $mac", "切换工作模式:$switch")
+        // 读取作业票信息
+//            val pkgList = ArrayList<BleTicketDataPackage>()
+//            val ticketInfo = bm.writeByResponse(token.buildBLEGetTicketInfoCMD()).getTicketPackageInfo()
+//            pkgList.add(ticketInfo)
+//            LogUtil.i("xiaoming $mac", "读取作业票:首包信息:$ticketInfo")
+//            // 校验是否有子包,如果有,继续读取子包数据
+//            for (idx in 1 until ticketInfo.pkgTotal) {
+//                val ticketSubPackageInfo = bm.writeByResponse(token.buildBLEGetTicketInfoCMD(idx, ticketInfo.pkgTotal)).getTicketPackageInfo()
+//                pkgList.add(ticketSubPackageInfo)
+//                LogUtil.i("xiaoming $mac", "读取作业票:子包信息:$ticketSubPackageInfo")
+//            }
+//            var datas = byteArrayOf()
+//            pkgList.forEach { datas += it.pkgData }
+//            LogUtil.i("xiaoming $mac", "读取作业票:${String(datas)}")
+    } else {
+        // 进行重连,这里可以封装尝试次数
+        bleKeyTest(application, mac)
     }
+    bm.disconnect()
 }
 
 /**

+ 124 - 0
app/src/main/java/com/iscs/bozzys/utils/CRC16.kt

@@ -0,0 +1,124 @@
+package com.iscs.bozzys.utils
+
+object CRC16 {
+    private val auchCRCHi = byteArrayOf(
+        0x00, 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(),
+        0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x01.toByte(),
+        0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(),
+        0x81.toByte(), 0x40.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x01.toByte(),
+        0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x01.toByte(), 0xC0.toByte(),
+        0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(),
+        0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x00.toByte(), 0xC1.toByte(),
+        0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(),
+        0x41.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x01.toByte(),
+        0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(),
+        0x81.toByte(), 0x40.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x00.toByte(), 0xC1.toByte(),
+        0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(),
+        0x41.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x01.toByte(),
+        0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(),
+        0x81.toByte(), 0x40.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(),
+        0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(),
+        0x80.toByte(), 0x41.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(),
+        0x41.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x00.toByte(), 0xC1.toByte(),
+        0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(),
+        0x41.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x00.toByte(), 0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte(), 0x01.toByte(), 0xC0.toByte(),
+        0x80.toByte(), 0x41.toByte(), 0x00.toByte(), 0xC1.toByte(), 0x81.toByte(),
+        0x40.toByte(), 0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(),
+        0x01.toByte(), 0xC0.toByte(), 0x80.toByte(), 0x41.toByte(), 0x00.toByte(),
+        0xC1.toByte(), 0x81.toByte(), 0x40.toByte()
+    )
+
+    private val auchCRCLo = byteArrayOf(
+        0x00.toByte(), 0xC0.toByte(), 0xC1.toByte(),
+        0x01.toByte(), 0xC3.toByte(), 0x03.toByte(), 0x02.toByte(), 0xC2.toByte(),
+        0xC6.toByte(), 0x06.toByte(), 0x07.toByte(), 0xC7.toByte(), 0x05.toByte(),
+        0xC5.toByte(), 0xC4.toByte(), 0x04.toByte(), 0xCC.toByte(), 0x0C.toByte(),
+        0x0D.toByte(), 0xCD.toByte(), 0x0F.toByte(), 0xCF.toByte(), 0xCE.toByte(),
+        0x0E.toByte(), 0x0A.toByte(), 0xCA.toByte(), 0xCB.toByte(), 0x0B.toByte(),
+        0xC9.toByte(), 0x09.toByte(), 0x08.toByte(), 0xC8.toByte(), 0xD8.toByte(),
+        0x18.toByte(), 0x19.toByte(), 0xD9.toByte(), 0x1B.toByte(), 0xDB.toByte(),
+        0xDA.toByte(), 0x1A.toByte(), 0x1E.toByte(), 0xDE.toByte(), 0xDF.toByte(),
+        0x1F.toByte(), 0xDD.toByte(), 0x1D.toByte(), 0x1C.toByte(), 0xDC.toByte(),
+        0x14.toByte(), 0xD4.toByte(), 0xD5.toByte(), 0x15.toByte(), 0xD7.toByte(),
+        0x17.toByte(), 0x16.toByte(), 0xD6.toByte(), 0xD2.toByte(), 0x12.toByte(),
+        0x13.toByte(), 0xD3.toByte(), 0x11.toByte(), 0xD1.toByte(), 0xD0.toByte(),
+        0x10.toByte(), 0xF0.toByte(), 0x30.toByte(), 0x31.toByte(), 0xF1.toByte(),
+        0x33.toByte(), 0xF3.toByte(), 0xF2.toByte(), 0x32.toByte(), 0x36.toByte(),
+        0xF6.toByte(), 0xF7.toByte(), 0x37.toByte(), 0xF5.toByte(), 0x35.toByte(),
+        0x34.toByte(), 0xF4.toByte(), 0x3C.toByte(), 0xFC.toByte(), 0xFD.toByte(),
+        0x3D.toByte(), 0xFF.toByte(), 0x3F.toByte(), 0x3E.toByte(), 0xFE.toByte(),
+        0xFA.toByte(), 0x3A.toByte(), 0x3B.toByte(), 0xFB.toByte(), 0x39.toByte(),
+        0xF9.toByte(), 0xF8.toByte(), 0x38.toByte(), 0x28.toByte(), 0xE8.toByte(),
+        0xE9.toByte(), 0x29.toByte(), 0xEB.toByte(), 0x2B.toByte(), 0x2A.toByte(),
+        0xEA.toByte(), 0xEE.toByte(), 0x2E.toByte(), 0x2F.toByte(), 0xEF.toByte(),
+        0x2D.toByte(), 0xED.toByte(), 0xEC.toByte(), 0x2C.toByte(), 0xE4.toByte(),
+        0x24.toByte(), 0x25.toByte(), 0xE5.toByte(), 0x27.toByte(), 0xE7.toByte(),
+        0xE6.toByte(), 0x26.toByte(), 0x22.toByte(), 0xE2.toByte(), 0xE3.toByte(),
+        0x23.toByte(), 0xE1.toByte(), 0x21.toByte(), 0x20.toByte(), 0xE0.toByte(),
+        0xA0.toByte(), 0x60.toByte(), 0x61.toByte(), 0xA1.toByte(), 0x63.toByte(),
+        0xA3.toByte(), 0xA2.toByte(), 0x62.toByte(), 0x66.toByte(), 0xA6.toByte(),
+        0xA7.toByte(), 0x67.toByte(), 0xA5.toByte(), 0x65.toByte(), 0x64.toByte(),
+        0xA4.toByte(), 0x6C.toByte(), 0xAC.toByte(), 0xAD.toByte(), 0x6D.toByte(),
+        0xAF.toByte(), 0x6F.toByte(), 0x6E.toByte(), 0xAE.toByte(), 0xAA.toByte(),
+        0x6A.toByte(), 0x6B.toByte(), 0xAB.toByte(), 0x69.toByte(), 0xA9.toByte(),
+        0xA8.toByte(), 0x68.toByte(), 0x78.toByte(), 0xB8.toByte(), 0xB9.toByte(),
+        0x79.toByte(), 0xBB.toByte(), 0x7B.toByte(), 0x7A.toByte(), 0xBA.toByte(),
+        0xBE.toByte(), 0x7E.toByte(), 0x7F.toByte(), 0xBF.toByte(), 0x7D.toByte(),
+        0xBD.toByte(), 0xBC.toByte(), 0x7C.toByte(), 0xB4.toByte(), 0x74.toByte(),
+        0x75.toByte(), 0xB5.toByte(), 0x77.toByte(), 0xB7.toByte(), 0xB6.toByte(),
+        0x76.toByte(), 0x72.toByte(), 0xB2.toByte(), 0xB3.toByte(), 0x73.toByte(),
+        0xB1.toByte(), 0x71.toByte(), 0x70.toByte(), 0xB0.toByte(), 0x50.toByte(),
+        0x90.toByte(), 0x91.toByte(), 0x51.toByte(), 0x93.toByte(), 0x53.toByte(),
+        0x52.toByte(), 0x92.toByte(), 0x96.toByte(), 0x56.toByte(), 0x57.toByte(),
+        0x97.toByte(), 0x55.toByte(), 0x95.toByte(), 0x94.toByte(), 0x54.toByte(),
+        0x9C.toByte(), 0x5C.toByte(), 0x5D.toByte(), 0x9D.toByte(), 0x5F.toByte(),
+        0x9F.toByte(), 0x9E.toByte(), 0x5E.toByte(), 0x5A.toByte(), 0x9A.toByte(),
+        0x9B.toByte(), 0x5B.toByte(), 0x99.toByte(), 0x59.toByte(), 0x58.toByte(),
+        0x98.toByte(), 0x88.toByte(), 0x48.toByte(), 0x49.toByte(), 0x89.toByte(),
+        0x4B.toByte(), 0x8B.toByte(), 0x8A.toByte(), 0x4A.toByte(), 0x4E.toByte(),
+        0x8E.toByte(), 0x8F.toByte(), 0x4F.toByte(), 0x8D.toByte(), 0x4D.toByte(),
+        0x4C.toByte(), 0x8C.toByte(), 0x44.toByte(), 0x84.toByte(), 0x85.toByte(),
+        0x45.toByte(), 0x87.toByte(), 0x47.toByte(), 0x46.toByte(), 0x86.toByte(),
+        0x82.toByte(), 0x42.toByte(), 0x43.toByte(), 0x83.toByte(), 0x41.toByte(),
+        0x81.toByte(), 0x80.toByte(), 0x40.toByte()
+    )
+
+    fun crc16(puchMsg: ByteArray, from: Int, to: Int): Int {
+        var uchCRCHi = 0xFF.toByte()
+        var uchCRCLo = 0xFF.toByte()
+        for (i in from..<to) {
+            val uIndex = (uchCRCHi.toInt() xor puchMsg[i].toInt()) and 0xff
+            uchCRCHi = (uchCRCLo.toInt() xor auchCRCHi[uIndex].toInt()).toByte()
+            uchCRCLo = auchCRCLo[uIndex]
+        }
+        return (((uchCRCHi.toInt()) shl 8 or ((uchCRCLo.toInt()) and 0xff))) and 0xffff
+    }
+}

+ 41 - 0
app/src/main/java/com/iscs/bozzys/utils/Exts.kt

@@ -24,4 +24,45 @@ fun List<String>.getRoleName(): String {
 fun List<String>?.isAdmin(): Boolean {
     if (this == null) return false
     return this.contains("super_admin")
+}
+
+/**
+ * 将字节数组转换为16进制字符串
+ */
+fun ByteArray.byteArrayToHexString(sep: String = ""): String {
+    return joinToString(sep) { "%02X".format(it) }
+}
+
+/**
+ * 16进制转自己数组
+ */
+fun String.hexToByteArray(): ByteArray {
+    return this.chunked(2)
+        .map { it.toInt(16).toByte() }
+        .toByteArray()
+}
+
+/**
+ * 将Int转为ByteArray数组
+ */
+fun Int.toByteArray(capability: Int = 2): ByteArray {
+    val bytes = ByteArray(capability)
+    for (i in 0 until capability) {
+        // 大端模式
+        // bytes[capability - i - 1] = ((this ushr (i * 8)) and 0xFF).toByte()
+        // 小端模式
+        bytes[i] = ((this ushr (i * 8)) and 0xFF).toByte()
+    }
+    return bytes
+}
+
+/**
+ * CRC16 校验值
+ * @return 两字节的校验值
+ */
+fun ByteArray.crc16(from: Int = 0, to: Int = size) : ByteArray {
+    val value = CRC16.crc16(this, from.coerceAtLeast(0), to.coerceAtMost(size))
+    val c1 = (0xff00 and value shr 8).toByte()
+    val c2 = (0xff and value).toByte()
+    return byteArrayOf(c1, c2)
 }

+ 3 - 0
app/src/main/java/com/iscs/bozzys/utils/ble/BleConnectResult.kt

@@ -0,0 +1,3 @@
+package com.iscs.bozzys.utils.ble
+
+data class BleConnectResult(val connected: Boolean = false)

+ 20 - 0
app/src/main/java/com/iscs/bozzys/utils/ble/BleFrame.kt

@@ -0,0 +1,20 @@
+package com.iscs.bozzys.utils.ble
+
+/**
+ * 构建蓝牙通信数据帧
+ *
+ * @param reqCode   请求码
+ * @param data      请求或响应数据
+ * @param rspCode   响应码
+ */
+data class BleFrame(val reqCode: ByteArray, val data: ByteArray, val rspCode: ByteArray, val writeUUID: String = "")
+
+/**
+ * 蓝牙作业票数据包信息
+ *
+ * @param pkgIdx    当前包索引
+ * @param pkgSize   当前包大小
+ * @param pkgData   当前包数据
+ * @param pkgTotal  总包数
+ */
+data class BleTicketDataPackage(val pkgIdx: Int, val pkgSize: Int, val pkgData: ByteArray, val pkgTotal: Int)

+ 172 - 0
app/src/main/java/com/iscs/bozzys/utils/ble/BleFrameExt.kt

@@ -0,0 +1,172 @@
+package com.iscs.bozzys.utils.ble
+
+import com.iscs.bozzys.utils.crc16
+import com.iscs.bozzys.utils.toByteArray
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+
+object BleFrameExt {
+
+    /**
+     * 构建蓝牙设备获取设备token的操作
+     */
+    fun buildBLEGetTokenCMD(): BleFrame {
+        return BleFrame(BleProtocol.REQ_GET_TOKEN, getUnixTime(), BleProtocol.RSP_GET_TOKEN)
+    }
+
+    /**
+     * 构建蓝牙设备获取蓝牙设备电量
+     */
+    fun ByteArray.buildBLEGetPowerCMD(): BleFrame {
+        return BleFrame(BleProtocol.REQ_POWER_STATUS, getUnixTime() + this, BleProtocol.RSP_POWER_STATUS)
+    }
+
+    /**
+     * 构建蓝牙获取当前设备状态
+     */
+    fun ByteArray.buildBLEGetStatusCMD(): BleFrame {
+        return BleFrame(BleProtocol.REQ_CURRENT_STATUS, getUnixTime() + this, BleProtocol.RSP_CURRENT_STATUS)
+    }
+
+    /**
+     * 切换当前工作模式
+     *
+     * @param runMode 有效运行模式 WORK STBY
+     */
+    fun ByteArray.buildBLESwitchRunModeCMD(runMode: BleRunMode): BleFrame {
+        var mode = 0x00
+        if (runMode == BleRunMode.WORK) {
+            mode = 0x01
+        } else if (runMode == BleRunMode.STBY) {
+            mode = 0x02
+        }
+        return BleFrame(BleProtocol.REQ_SWITCH_MODE, byteArrayOf(mode.toByte()) + getUnixTime() + this, BleProtocol.RSP_SWITCH_MODE)
+    }
+
+    /**
+     * 构建用于作业票下发的指令集合
+     */
+    fun ByteArray.buildBLETicketDataCMDList(json: String): List<BleFrame> {
+        val cmds = ArrayList<BleFrame>()
+        // 先将数据转换为字节数组
+        val pkg = json.toByteArray()
+        // 包大小
+        val pkgSize = pkg.size
+        // 分包数
+        val pkgCount = (pkgSize + 128 - 1) / 128
+        for (idx in 0 until pkgCount) {
+            val endIdx = if ((idx + 1) == pkgCount) pkgSize else ((idx + 1) * 128)
+            // 将包做切片操作
+            val subPkg = pkg.copyOfRange(idx * 128, endIdx)
+            // 封装数据包操作
+            val data = pkgCount.toByteArray() + idx.toByteArray() + subPkg.crc16(0, subPkg.size) + subPkg.size.toByteArray() + subPkg
+            // 装包处理
+            cmds.add(
+                BleFrame(
+                    BleProtocol.REQ_SEND_WORK_TICKET,
+                    (data.size + 1).toByteArray(1) + 0x02.toByteArray(1) + data + getUnixTime() + this,
+                    BleProtocol.RSP_SEND_WORK_TICKET,
+                    BleProtocol.WRITE_UUID
+                )
+            )
+        }
+        return cmds
+    }
+
+    /**
+     * 构建获取作业票信息命令
+     */
+    fun ByteArray.buildBLEGetTicketInfoCMD(idx: Int = 0, pkgTotal: Int = 0): BleFrame {
+        if (idx > 0) {
+            return BleFrame(
+                BleProtocol.REQ_WORK_TICKET_RESULT_PART,
+                idx.toByteArray() + pkgTotal.toByteArray() + byteArrayOf(0x00) + getUnixTime() + this,
+                BleProtocol.RSP_WORK_TICKET_RESULT
+            )
+        }
+        return BleFrame(BleProtocol.REQ_WORK_TICKET_RESULT, getUnixTime() + this, BleProtocol.RSP_WORK_TICKET_RESULT)
+    }
+
+    /**
+     * 获取token
+     */
+    fun BleFrame.getToken(): ByteArray {
+        if (this.rspCode.contentEquals(BleProtocol.RSP_GET_TOKEN)) {
+            return this.data
+        }
+        return byteArrayOf()
+    }
+
+    /**
+     * 获取当前运行模式
+     */
+    fun BleFrame.getRunMode(): BleRunMode {
+        if (this.rspCode.contentEquals(BleProtocol.RSP_CURRENT_STATUS)) {
+            return when (this.data[0]) {
+                0x01.toByte() -> BleRunMode.WORK
+                0x02.toByte() -> BleRunMode.STBY
+                0x03.toByte() -> BleRunMode.FAILURE
+                else -> BleRunMode.NONE
+            }
+        }
+        return BleRunMode.NONE
+    }
+
+    /**
+     * 获取当前设备电量
+     */
+    fun BleFrame.getPower(): Int {
+        if (this.rspCode.contentEquals(BleProtocol.RSP_POWER_STATUS)) {
+            return this.data[0].toInt()
+        }
+        return -1
+    }
+
+    /**
+     * 获取当前工作模式切换是否成功
+     */
+    fun BleFrame.getSwitchRunModeResult(): Boolean {
+        if (this.rspCode.contentEquals(BleProtocol.RSP_SWITCH_MODE)) {
+            return this.data[1].toInt() == 1
+        }
+        return false
+    }
+
+    /**
+     * 响应发送工作票结果
+     *
+     * @return 0-成功 1-失败 2-超时 13-idx超出范围
+     */
+    fun BleFrame.getSendTicketResult(): Int {
+        if (this.rspCode.contentEquals(BleProtocol.RSP_SEND_WORK_TICKET)) {
+            return this.data[4].toInt()
+        }
+        return 99
+    }
+
+    fun BleFrame.getTicketPackageInfo(): BleTicketDataPackage {
+        if (this.rspCode.contentEquals(BleProtocol.RSP_WORK_TICKET_RESULT)) {
+            // 解析数据
+            val pkgCount = this.data[2] + this.data[3]
+            val pkgIdx = this.data[4] + this.data[5]
+            val pkgSize = this.data[8].toUByte() + this.data[9].toUByte()
+            val pkgData = this.data.copyOfRange(10, pkgSize.toInt() + 10)
+            // ISCSLog.i("getTicketPackageInfo", "包信息:总包数:$pkgCount, 当前包:$pkgIdx, 当前包大小:$pkgSize, 当前包数据:${pkgData.byteArrayToHexString("")}")
+            return BleTicketDataPackage(pkgIdx, pkgSize.toInt(), pkgData, pkgCount)
+        }
+        return BleTicketDataPackage(0, 0, byteArrayOf(), 0)
+    }
+
+}
+
+private fun getUnixTime(): ByteArray {
+    val tempArr = (System.currentTimeMillis() / 1000).toByteArray()
+    val timeStampArr = byteArrayOf(tempArr[0], tempArr[1], tempArr[2], tempArr[3])
+    return timeStampArr
+}
+
+private fun Long.toByteArray(): ByteArray {
+    return ByteBuffer.allocate(java.lang.Long.BYTES)
+        .order(ByteOrder.LITTLE_ENDIAN)
+        .putLong(this).array()
+}

+ 315 - 0
app/src/main/java/com/iscs/bozzys/utils/ble/BleManager.kt

@@ -0,0 +1,315 @@
+package com.iscs.bozzys.utils.ble
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothGatt
+import android.bluetooth.BluetoothGattCallback
+import android.bluetooth.BluetoothGattCharacteristic
+import android.bluetooth.BluetoothGattDescriptor
+import android.bluetooth.BluetoothManager
+import android.bluetooth.BluetoothProfile
+import android.bluetooth.le.ScanCallback
+import android.bluetooth.le.ScanFilter
+import android.bluetooth.le.ScanResult
+import android.bluetooth.le.ScanSettings
+import android.os.Build
+import android.os.SystemClock
+import com.iscs.bozzys.utils.LogUtil
+import com.iscs.bozzys.utils.byteArrayToHexString
+import com.iscs.bozzys.utils.hexToByteArray
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * 蓝牙连接管理器
+ */
+class BleManager(
+    val app: Application,
+    val mac: String = "",
+    val mtu: Int = 500,
+    val needIndicate: Boolean = true,
+    val writeUUID: String = BleProtocol.INDICATE_UUID,
+    val serviceUUID: String = BleProtocol.SERVICE_UUID,
+) {
+
+    companion object {
+        private const val TAG = "BLEManager"
+    }
+
+    // 蓝牙管理器
+    private val bm = app.getSystemService(BluetoothManager::class.java)
+
+    // 蓝牙适配器
+    private val ba = bm.adapter
+
+    // 当前连接的设备
+    private var device: BluetoothDevice? = null
+
+    // gatt连接对象
+    private var gatt: BluetoothGatt? = null
+
+    // 连接回调
+    private var doneConnect: CancellableContinuation<BleConnectResult>? = null
+
+    // 待响应式的请求数据接收池
+    private val receiverPool = ConcurrentHashMap<String, CompletableDeferred<BleFrame>>()
+
+    // 蓝牙扫描回调
+    private val scanCallback = object : ScanCallback() {
+
+        override fun onScanResult(callbackType: Int, result: ScanResult?) {
+            super.onScanResult(callbackType, result)
+            LogUtil.i(TAG, "scan() onScanResult() ${result?.device?.address}")
+            if (result != null && result.device != null) {
+                device = result.device
+                // 查找到设备后,停止扫描并且执行连接操作
+                stopScan()
+                // 执行连接蓝牙设备
+                innerConnect()
+            }
+        }
+
+        override fun onScanFailed(errorCode: Int) {
+            super.onScanFailed(errorCode)
+            LogUtil.i(TAG, "scan() onScanFailed() $errorCode")
+            // 扫描失败重新执行扫描
+            scan()
+        }
+
+    }
+
+    /**
+     * 蓝牙Gatt连接回调
+     */
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private val gattCallback = object : BluetoothGattCallback() {
+
+        @SuppressLint("MissingPermission")
+        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
+            if (newState == BluetoothProfile.STATE_CONNECTED) {
+                LogUtil.i(TAG, "gattCallback onConnectionStateChange() gatt connected")
+                gatt.discoverServices()
+            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+                LogUtil.i(TAG, "gattCallback onConnectionStateChange() gatt disconnected")
+                if (!(this@BleManager.doneConnect?.isCompleted ?: false)) {
+                    this@BleManager.doneConnect?.resume(BleConnectResult(false), null)
+                }
+            }
+        }
+
+        @SuppressLint("MissingPermission")
+        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
+            if (status == BluetoothGatt.GATT_SUCCESS) {
+                LogUtil.i(TAG, "gattCallback onServicesDiscovered() find services success")
+                // 执行设置mtu操作
+                gatt.requestMtu(mtu)
+            }
+        }
+
+        override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
+            super.onMtuChanged(gatt, mtu, status)
+            LogUtil.i(TAG, "gattCallback onMtuChanged() mtu $mtu")
+            Thread {
+                val indicateOk = enableIndicate()
+                SystemClock.sleep(1000)
+                if (needIndicate) {
+                    if (!(this@BleManager.doneConnect?.isCompleted ?: false)) this@BleManager.doneConnect?.resume(BleConnectResult(indicateOk), null)
+                } else {
+                    if (!(this@BleManager.doneConnect?.isCompleted ?: false)) this@BleManager.doneConnect?.resume(BleConnectResult(true), null)
+                }
+            }.start()
+        }
+
+        @SuppressLint("MissingPermission")
+        override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, status: Int) {
+            super.onCharacteristicRead(gatt, characteristic, value, status)
+            LogUtil.i(TAG, "gattCallback onCharacteristicRead()")
+        }
+
+        override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
+            super.onCharacteristicWrite(gatt, characteristic, status)
+            LogUtil.w(TAG, "gattCallback ---> ${characteristic.value.byteArrayToHexString(" ")}")
+        }
+
+        override fun onDescriptorRead(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, value: ByteArray) {
+            super.onDescriptorRead(gatt, descriptor, status, value)
+            LogUtil.i(TAG, "gattCallback onDescriptorRead() data write success")
+        }
+
+        override fun onDescriptorRead(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) {
+            super.onDescriptorRead(gatt, descriptor, status)
+            LogUtil.i(TAG, "gattCallback onDescriptorRead() data write success")
+        }
+
+        override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?) {
+            super.onCharacteristicChanged(gatt, characteristic)
+            LogUtil.w(TAG, "gattCallback <--- ${characteristic?.value?.byteArrayToHexString(" ")}")
+            val data = characteristic?.value ?: byteArrayOf()
+            var key = ""
+            receiverPool.forEach { item ->
+                if (data.byteArrayToHexString().startsWith(item.key.split("_")[1])) {
+                    // 找到指定响应体
+                    key = item.key
+                    return@forEach
+                }
+            }
+            if (key.isNotEmpty()) {
+                val deferred = receiverPool.remove(key)
+                if (deferred != null && !deferred.isCompleted) {
+                    val spl = key.split("_")
+                    val rspCodeLen = spl[1].length / 2
+                    deferred.complete(BleFrame(spl[0].hexToByteArray(), data.copyOfRange(rspCodeLen, data.size), spl[1].hexToByteArray()))
+                }
+            }
+        }
+
+        override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) {
+            super.onCharacteristicChanged(gatt, characteristic, value)
+            LogUtil.w(TAG, "gattCallback <--- ${value.byteArrayToHexString(" ")}")
+            var key = ""
+            receiverPool.forEach { item ->
+                if (value.byteArrayToHexString().startsWith(item.key.split("_")[1])) {
+                    // 找到指定响应体
+                    key = item.key
+                    return@forEach
+                }
+            }
+            if (key.isNotEmpty()) {
+                val deferred = receiverPool.remove(key)
+                if (deferred != null && !deferred.isCompleted) {
+                    val spl = key.split("_")
+                    val rspCodeLen = spl[1].length / 2
+                    deferred.complete(BleFrame(spl[0].hexToByteArray(), value.copyOfRange(rspCodeLen, value.size), spl[1].hexToByteArray()))
+                }
+            }
+        }
+    }
+
+    /**
+     * 协程方式的连接
+     */
+    suspend fun connect(): BleConnectResult = suspendCancellableCoroutine { block ->
+        this.doneConnect = block
+        // 开始执行扫描连接
+        scan()
+    }
+
+    /**
+     * 查找设备
+     */
+    @SuppressLint("MissingPermission")
+    private fun scan() {
+        if (ba == null || !ba.isEnabled) {
+            // 蓝牙不可用
+            LogUtil.e(TAG, "scan() bluetooth unused")
+            if (!(this.doneConnect?.isCompleted ?: false)) this.doneConnect?.resume(BleConnectResult(false), null)
+            return
+        }
+        LogUtil.i(TAG, "scan() bluetooth started")
+        // 获取扫描对象
+        val scan = ba.bluetoothLeScanner
+        // 设置扫描过滤
+        val filters = ArrayList<ScanFilter>()
+        if (mac.isNotEmpty()) filters.add(ScanFilter.Builder().setDeviceAddress(mac).build())
+        // 设置蓝牙扫描频率高 SCAN_MODE_LOW_LATENCY
+        // 设置蓝牙扫描频率中 SCAN_MODE_BALANCED
+        val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
+        scan.startScan(filters, settings, scanCallback)
+    }
+
+    @SuppressLint("MissingPermission")
+    fun stopScan() {
+        try {
+            ba.bluetoothLeScanner.stopScan(scanCallback)
+        } catch (e: Exception) {
+            LogUtil.e(TAG, "stopScan() stop scan failed $e")
+        }
+    }
+
+    /**
+     * 连接设备,内部方法
+     */
+    @SuppressLint("MissingPermission")
+    private fun innerConnect() {
+        // context -> app
+        // autoConnect -> false 避免设备连接异常
+        try {
+            gatt = device?.connectGatt(app, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
+        } catch (e: Exception) {
+            LogUtil.e(TAG, "connect() connect to device ${device?.address} failed $e")
+        }
+    }
+
+    /**
+     * 断开连接
+     */
+    @SuppressLint("MissingPermission")
+    fun disconnect() {
+        device?.let {
+            gatt?.disconnect()
+            gatt?.close()
+        }
+    }
+
+    /**
+     * 是否使能Indicate
+     */
+    @SuppressLint("MissingPermission")
+    private fun enableIndicate(uuid: String = writeUUID): Boolean {
+        gatt?.getService(UUID.fromString(serviceUUID))?.getCharacteristic(UUID.fromString(uuid))?.let {
+            // 开启通知
+            val open = gatt?.setCharacteristicNotification(it, true)
+            LogUtil.i(TAG, "enableIndicate() open notification $open")
+            val descriptor = it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
+            LogUtil.i(TAG, "enableIndicate() get descriptor $descriptor")
+            if (open != true || descriptor == null) return false
+
+            // 写入desc
+            val enable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                gatt?.writeDescriptor(descriptor, BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)
+            } else {
+                descriptor.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
+                gatt?.writeDescriptor(descriptor)
+            }
+            LogUtil.i(TAG, "enableIndicate() indicate enable $enable")
+            return enable == true
+        }
+        return false
+    }
+
+    /**
+     * 写数据,带响应
+     */
+    @SuppressLint("MissingPermission")
+    suspend fun writeByResponse(frame: BleFrame) = withContext(Dispatchers.IO) {
+        val deferred = CompletableDeferred<BleFrame>()
+        val writeUUID = if (frame.writeUUID.isNotEmpty()) UUID.fromString(frame.writeUUID) else UUID.fromString(writeUUID)
+        receiverPool["${frame.reqCode.byteArrayToHexString()}_${frame.rspCode.byteArrayToHexString()}"] = deferred
+        // 发送数据方法兼容处理
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            gatt?.getService(UUID.fromString(serviceUUID))?.getCharacteristic(writeUUID)?.let {
+                gatt?.writeCharacteristic(it, frame.reqCode + frame.data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
+            }
+        } else {
+            val characteristic = gatt?.getService(UUID.fromString(serviceUUID))?.getCharacteristic(writeUUID)
+            characteristic?.value = frame.reqCode + frame.data
+            characteristic?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
+            gatt?.writeCharacteristic(characteristic)
+        }
+        try {
+            withTimeout(1000) { deferred.await() }
+        } catch (_: TimeoutCancellationException) {
+            BleFrame(byteArrayOf(), byteArrayOf(), byteArrayOf())
+        }
+    }
+
+}

+ 56 - 0
app/src/main/java/com/iscs/bozzys/utils/ble/BleProtocol.kt

@@ -0,0 +1,56 @@
+package com.iscs.bozzys.utils.ble
+
+/**
+ * 封装用于蓝牙通信的协议
+ */
+object BleProtocol {
+
+    // 服务特征
+    const val SERVICE_UUID = "0000FEE7-0000-1000-8000-00805F9B34FB"
+
+    // 写入其他数据特征
+    const val INDICATE_UUID = "0000FED1-0000-1000-8000-00805F9B34FB"
+
+    // 写作业票数据特征
+    const val WRITE_UUID = "0000FED2-0000-1000-8000-00805F9B34FB"
+
+    // 获取令牌,需增加4字节的时间戳,总长8个字节长度
+    val REQ_GET_TOKEN = byteArrayOf(0x01, 0x01, 0x05, 0x00)
+
+    // 获取令牌响应,最后4个是token,总长15个字节长度
+    val RSP_GET_TOKEN = byteArrayOf(0x01, 0x02, 0x04)
+
+    // 获取钥匙电量
+    val REQ_POWER_STATUS = byteArrayOf(0x03, 0x01, 0x01, 0x03)
+
+    // 获取钥匙电量响应
+    val RSP_POWER_STATUS = byteArrayOf(0x03, 0x02, 0x03, 0x03)
+
+    // 获取当前设备请求
+    val REQ_CURRENT_STATUS = byteArrayOf(0x03, 0x01, 0x01, 0x01)
+
+    // 获取当前设备响应
+    val RSP_CURRENT_STATUS = byteArrayOf(0x03, 0x02, 0x02, 0x01)
+
+    // 工作模式切换请求
+    val REQ_SWITCH_MODE = byteArrayOf(0x02, 0x01, 0x02, 0x01)
+
+    // 工作模式切换响应
+    val RSP_SWITCH_MODE = byteArrayOf(0x02, 0x02, 0x03, 0x01)
+
+    // 工作票下发
+    val REQ_SEND_WORK_TICKET = byteArrayOf(0x02, 0x01)
+
+    // 工作票下发响应
+    val RSP_SEND_WORK_TICKET = byteArrayOf(0x02, 0x02, 0x06, 0x02)
+
+    // 获取设备工作票完成情况
+    val REQ_WORK_TICKET_RESULT = byteArrayOf(0x03, 0x01, 0x01, 0x02)
+
+    // 获取设备工作票完成情况响应
+    val RSP_WORK_TICKET_RESULT = byteArrayOf(0x03, 0x02)
+
+    // 获取设备工作票完成情况分包
+    val REQ_WORK_TICKET_RESULT_PART = byteArrayOf(0x03, 0x01, 0x06, 0x02)
+
+}

+ 8 - 0
app/src/main/java/com/iscs/bozzys/utils/ble/BleRunMode.kt

@@ -0,0 +1,8 @@
+package com.iscs.bozzys.utils.ble
+
+enum class BleRunMode {
+    WORK,       // 工作模式
+    STBY,       // 待机模式
+    FAILURE,    // 故障模式
+    NONE,       // 未知
+}