Browse Source

蓝牙通信封装

bjb 1 week ago
parent
commit
2216ab0668

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

@@ -1,6 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
+    <!--  蓝牙相关权限  -->
+    <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:allowBackup="true"
         android:dataExtractionRules="@xml/data_extraction_rules"

+ 9 - 0
app/src/main/java/com/iscs/comm/MainActivity.kt

@@ -1,6 +1,7 @@
 package com.iscs.comm
 
 import android.os.Bundle
+import android.util.Log
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.activity.enableEdgeToEdge
@@ -25,7 +26,9 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import com.iscs.comm.entity.device.Device
+import com.iscs.comm.extension.CommFrameExt
 import com.iscs.comm.intf.IDeviceListener
+import com.iscs.comm.manager.BLEManager
 import com.iscs.comm.ui.theme.CommDemoTheme
 
 class MainActivity : ComponentActivity() {
@@ -58,6 +61,12 @@ class MainActivity : ComponentActivity() {
             CommManager.addOnDeviceListener(listener)
             // SDK初始化
             CommManager.init()
+            // 测试蓝牙扫描
+            val bm = BLEManager(this@MainActivity.application, mac = "CC:BA:97:21:71:E6")
+            val result = bm.connect()
+            if (result.connected) {
+                bm.writeByResponse(CommFrameExt.buildBLEGetTokenCMD())
+            }
         }
         DisposableEffect("") {
             // 移除页面监听

+ 3 - 0
transport/src/main/java/com/iscs/comm/entity/BleConnectResult.kt

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

+ 10 - 0
transport/src/main/java/com/iscs/comm/entity/BleFrame.kt

@@ -0,0 +1,10 @@
+package com.iscs.comm.entity
+
+/**
+ * 构建蓝牙通信数据帧
+ *
+ * @param reqCode   请求码
+ * @param data      请求或响应数据
+ * @param rspCode   响应码
+ */
+data class BleFrame(val reqCode: ByteArray, val data: ByteArray, val rspCode: ByteArray)

+ 26 - 0
transport/src/main/java/com/iscs/comm/extension/CommFrameExt.kt

@@ -1,6 +1,7 @@
 package com.iscs.comm.extension
 
 import com.iscs.comm.entity.Frame
+import com.iscs.comm.protocol.BLEProtocol
 import com.iscs.comm.protocol.CanProtocol.CAN_ID_BASE
 import com.iscs.comm.protocol.CanProtocol.CAN_LOCK_RFID
 import com.iscs.comm.protocol.CanProtocol.CAN_READ_CMD
@@ -8,6 +9,8 @@ import com.iscs.comm.protocol.CanProtocol.CAN_SLOT_RW
 import com.iscs.comm.protocol.CanProtocol.CAN_SLOT_STATUS
 import com.iscs.comm.protocol.CanProtocol.CAN_WRITE_CMD_2BYTE
 import com.iscs.comm.utils.ISCSLog
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
 
 /**
  * 构建读CAN总线下设备信息命令集合
@@ -88,4 +91,27 @@ fun Frame.buildCtrlReadDeviceStatus(): Frame {
             0x00, 0x00, 0x00, 0x00, 0x00
         )
     }
+}
+
+object CommFrameExt {
+
+    /**
+     * 构建蓝牙设备获取设备token的操作
+     */
+    fun buildBLEGetTokenCMD(): ByteArray {
+        return BLEProtocol.REQ_GET_TOKEN + getUnixTime()
+    }
+
+}
+
+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()
 }

+ 242 - 0
transport/src/main/java/com/iscs/comm/manager/BLEManager.kt

@@ -0,0 +1,242 @@
+package com.iscs.comm.manager
+
+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.comm.entity.BleConnectResult
+import com.iscs.comm.entity.BleFrame
+import com.iscs.comm.extension.byteArrayToHexString
+import com.iscs.comm.protocol.BLEProtocol
+import com.iscs.comm.utils.ISCSLog
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.suspendCancellableCoroutine
+import java.util.UUID
+
+/**
+ * 蓝牙连接管理器
+ */
+class BLEManager(
+    val app: Application,
+    val mac: String = "",
+    val mtu: Int = 500,
+    val needIndicate: Boolean = true,
+    val indicateUUID: 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 scanCallback = object : ScanCallback() {
+
+        override fun onScanResult(callbackType: Int, result: ScanResult?) {
+            super.onScanResult(callbackType, result)
+            ISCSLog.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)
+            ISCSLog.i(TAG, "scan() onScanFailed() $errorCode")
+        }
+
+    }
+
+    /**
+     * 蓝牙Gatt连接回调
+     */
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private val gattCallback = object : BluetoothGattCallback() {
+
+        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
+            if (newState == BluetoothProfile.STATE_CONNECTED) {
+                ISCSLog.i(TAG, "gattCallback onConnectionStateChange() gatt connected")
+                gatt.discoverServices()
+            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+                ISCSLog.i(TAG, "gattCallback onConnectionStateChange() gatt disconnected")
+            }
+        }
+
+        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
+            if (status == BluetoothGatt.GATT_SUCCESS) {
+                ISCSLog.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)
+            ISCSLog.i(TAG, "gattCallback onMtuChanged() mtu $mtu")
+            Thread {
+                val indicateOk = enableIndicate()
+                SystemClock.sleep(2000)
+                if (needIndicate) {
+                    this@BLEManager.doneConnect?.resume(BleConnectResult(indicateOk), null)
+                } else {
+                    this@BLEManager.doneConnect?.resume(BleConnectResult(true), null)
+                }
+            }.start()
+        }
+
+        override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, status: Int) {
+            super.onCharacteristicRead(gatt, characteristic, value, status)
+            ISCSLog.i(TAG, "gattCallback onCharacteristicRead() ${gatt.readCharacteristic(characteristic)} ${value.contentToString()}")
+        }
+
+        override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
+            super.onCharacteristicWrite(gatt, characteristic, status)
+            ISCSLog.i(TAG, "gattCallback onCharacteristicWrite() data write success ${characteristic.value.byteArrayToHexString(" ")}")
+        }
+
+        override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?) {
+            super.onCharacteristicChanged(gatt, characteristic)
+            ISCSLog.i(TAG, "gattCallback onCharacteristicChanged() get notification data ${characteristic?.value?.byteArrayToHexString(" ")}")
+        }
+
+        override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) {
+            super.onCharacteristicChanged(gatt, characteristic, value)
+            ISCSLog.i(TAG, "gattCallback onCharacteristicChanged() get notification data ${value.byteArrayToHexString(" ")}")
+        }
+    }
+
+    /**
+     * 协程方式的连接
+     */
+    suspend fun connect(): BleConnectResult = suspendCancellableCoroutine { block ->
+        this.doneConnect = block
+        // 开始执行扫描连接
+        scan()
+    }
+
+    /**
+     * 查找设备
+     */
+    @SuppressLint("MissingPermission")
+    private fun scan() {
+        if (ba == null || !ba.isEnabled) {
+            // 蓝牙不可用
+            ISCSLog.e(TAG, "scan() bluetooth unused")
+            this.doneConnect?.resume(BleConnectResult(false), null)
+            return
+        }
+        // 获取扫描对象
+        val scan = ba.bluetoothLeScanner
+        // 设置扫描过滤
+        val filters = ArrayList<ScanFilter>()
+        if (mac.isNotEmpty()) filters.add(ScanFilter.Builder().setDeviceAddress(mac).build())
+        // 设置蓝牙扫描频率高
+        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) {
+            ISCSLog.e(TAG, "stopScan() stop scan failed $e")
+        }
+    }
+
+    /**
+     * 连接设备,内部方法
+     */
+    private fun innerConnect() {
+        // context -> app
+        // autoConnect -> false 避免设备连接异常
+        try {
+            gatt = device?.connectGatt(app, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
+        } catch (e: Exception) {
+            ISCSLog.e(TAG, "connect() connect to device ${device?.address} failed $e")
+        }
+    }
+
+    /**
+     * 断开连接
+     */
+    fun disconnect() {
+        device?.let { gatt?.close() }
+    }
+
+    /**
+     * 是否使能Indicate
+     */
+    private fun enableIndicate(): Boolean {
+        gatt?.getService(UUID.fromString(serviceUUID))?.getCharacteristic(UUID.fromString(indicateUUID))?.let {
+            // 开启通知
+            val open = gatt?.setCharacteristicNotification(it, true)
+            ISCSLog.i(TAG, "enableIndicate() open notification $open")
+            val descriptor = it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
+            ISCSLog.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)
+            }
+            ISCSLog.i(TAG, "enableIndicate() indicate enable $enable")
+            return enable == true
+        }
+        return false
+    }
+
+    /**
+     * 写数据,带响应
+     */
+    fun writeByResponse(frame: BleFrame) {
+        // 发送数据方法兼容处理
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            gatt?.getService(UUID.fromString(serviceUUID))?.getCharacteristic(UUID.fromString(indicateUUID))?.let {
+                ISCSLog.i(TAG, "writeByResponse() write characteristic find result ${it.uuid}")
+                gatt?.writeCharacteristic(it, frame.reqCode + frame.data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
+            }
+        } else {
+            val characteristic = gatt?.getService(UUID.fromString(serviceUUID))?.getCharacteristic(UUID.fromString(indicateUUID))
+            ISCSLog.i(TAG, "writeByResponse() write characteristic find result ${characteristic?.uuid}")
+            characteristic?.value = frame.reqCode + frame.data
+            characteristic?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
+            gatt?.writeCharacteristic(characteristic)
+        }
+    }
+
+}

+ 20 - 0
transport/src/main/java/com/iscs/comm/protocol/BLEProtocol.kt

@@ -0,0 +1,20 @@
+package com.iscs.comm.protocol
+
+/**
+ * 封装用于蓝牙通信的协议
+ */
+object BLEProtocol {
+
+    // 服务特征
+    const val SERVICE_UUID = "0000FEE7-0000-1000-8000-00805F9B34FB"
+
+    // 写入特征
+    const val INDICATE_UUID = "0000FED1-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)
+
+}