RxJava Android Bluetooth 經典藍牙 SDK 項目開發(fā)

Android 經典藍牙項目開發(fā):

分三部分

一: btcore (服務端和客戶端的核心代碼)
二: 服務端使用實例
三: 客戶端使用實例


btcore 分服務端和客戶端

服務端:
1: 啟動服務監(jiān)聽客戶端請求
2: 處理客戶端數(shù)據(jù)請求并返回數(shù)據(jù)
客戶端:
1: 連接服務端 (掃描)
2: 數(shù)據(jù)處理(數(shù)據(jù)組轉、隊列管理纠脾、超時處理丧诺、并發(fā))
3: 重連


使用藍牙前需要判斷藍牙是否可用虽画、開關是否開啟潜圃、是否授權
/**
 * 藍牙是否可用
 */
fun bluetoothEnable() : Boolean {
    return bluetoothAdapter != null
}

/**
 * 藍牙是否打開
 */
fun bluetoothIsOpen() : Boolean {
    return bluetoothAdapter.isEnabled ?: false
}

/**
 * 檢查權限
 */
fun havePermission() : Boolean {
    return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
        ActivityCompat.checkSelfPermission(content, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
    } else {
        ActivityCompat.checkSelfPermission(content, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
    }
}
服務端設置可被掃描連接 (0表示持續(xù)可掃描)
// 屬性
private lateinit var searchDeviceLauncher: ActivityResultLauncher<Intent>

// onCreate 中初始化
searchDeviceLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
    bluetoothManager!!.startAdvertiser()
}

// 需要啟動時執(zhí)行
private fun startDiscoverable() {
    var requestDiscoverIntent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)
    requestDiscoverIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0)
    searchDeviceLauncher.launch(requestDiscoverIntent)
}
一: 服務端核心類
1: AcceptThread 等待客戶端連接線程(阻塞式)
// 核心代碼
override fun run() {
    super.run()
    var shouldLoop : Boolean = true

    while (shouldLoop) {
        btSocket = try {
            Log.i("AcceptThread", " 服務正在等待監(jiān)聽..... ")
            serverSocket?.accept()
        } catch (e: Exception) {
            shouldLoop = false
            null
        }
        btSocket?.also {
            serverSocket?.close()
            shouldLoop = false
            // 通知有客戶端設備連接
            acceptSubject.onNext(it)

            Log.i("BluetoothSocket === : ", "$it")
        }
    }
}
2: CommunicationThread 通訊句柄類(客戶端也需要), 建立連接后的 BluetoothSocket 可以打開 輸入輸出兩個數(shù)據(jù)流芥驳,讀和寫數(shù)據(jù)
// 核心代碼

init {
    try {
        inputStream = DataInputStream(socket.inputStream)
        outputStream = DataOutputStream(socket.outputStream)

        Log.i(TAG, "會話建立完成")

    } catch (e: IOException) {
        Log.e(TAG, "獲取輸出輸入流失敗")
    }
}

override fun run() {
    super.run()
    var numBytes: Int
    while (true) {
        val mmBuffer: ByteArray = ByteArray(2000)
        // 阻塞 收數(shù)據(jù)
        if (inputStream != null && socket.isConnected) {
            numBytes = try {
                Log.i(TAG, "run: reading")
                inputStream!!.read(mmBuffer)
            } catch (e: Exception) {
                Log.e(TAG, "讀取數(shù)據(jù)異常 $e")
                readStateSubject.onNext(true)
                break
            }
            if (numBytes > 0) {
                val resultByteArray: ByteArray = ByteArray(numBytes)
                System.arraycopy(mmBuffer, 0, resultByteArray, 0, numBytes)
                dataSubject.onNext(Pair<Int, ByteArray>(numBytes, resultByteArray))
            }
        }
    }
}

/**
 * 寫數(shù)據(jù)
 */
fun write(bytes: ByteArray) : Boolean {
    if (!socket.isConnected) {
        return false
    }
    return try {
        outputStream?.write(bytes)
        outputStream?.flush()
        true
    } catch (e: IOException) {
        false
    }
}
3: BluetoothServerManger 管理連接介粘、數(shù)據(jù)處理、狀態(tài)監(jiān)聽
// 核心代碼

 /**
 * 作為服務端啟動監(jiān)聽(廣播)
 */
fun startAdvertiser() {
    if (acceptThread == null) {
        acceptThread = AcceptThread(uuid = BluetoothServerManger.serverUUID)
        acceptThread!!.acceptSubject
            .subscribeBy(
                onNext = {
                    bluetoothSocket = it
                    startServerCommunication(it)
                }
            )
        acceptThread!!.start()
    }
}

/**
 * 有客戶端接入晚树,建立連接
 */
private fun startServerCommunication(bs: BluetoothSocket) {
    serverCommThread = CommunicationThread(bs)

    serverCommThread!!.readStateSubject
        .subscribeBy(onNext = {
            reStartAdvertiser()
        }, onError = {

        })

    // 收到客戶端數(shù)據(jù)
    serverCommThread!!.dataSubject
        .subscribeBy(
            onNext = {
                val valueString = it.second.toString(Charset.defaultCharset())
                var d = String(it.second)
                val string2 = String(it.second, Charsets.UTF_16)
                Log.i(BluetoothClientManager.TAG, "服務端收到數(shù)據(jù) size: ${it.first}; data: $d $valueString  ${it.second} $string2")
                val receiverData =  it.second
                // 解析報文頭為: AABB姻采, 獲取到 taskId 用于返回
                if (receiverData.size > 4 &&
                    receiverData[0] == 0xAA.toByte() &&
                    receiverData[1] == 0xBB.toByte()) {
                    tempTask = BTTask()
                    val rTaskId = (receiverData[3].toInt() and 0xFF shl 8) or (receiverData[2].toInt() and 0xFF)
                    tempTask!!.taskId = rTaskId
                }
                if (tempTask == null) {
                    return@subscribeBy
                }
                tempTask?.addBuffer(receiverData)
                // 檢測數(shù)據(jù)接受完成, 返回給客戶端
                if (tempTask?.checkComplete() == true) {
                    val responseByte = byteArrayOf(1, 2, 3, 4, 5)
                    responseData(tempTask!!.buildCmdContent(responseByte))
                    Log.i(BluetoothClientManager.TAG, "服務端回復了數(shù)據(jù)")
                }
            },
            onError = {
                Log.i(BluetoothClientManager.TAG, "startServerCommunication: 出錯了")
            }
        )
    serverCommThread!!.start()
}

/**
 * 監(jiān)聽手機藍牙開關變化,做出處理
 */
private fun registerBluetoothStateEvent() {
    val intent =  IntentFilter()
    intent.addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
    intent.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
    intent.addAction(BluetoothDevice.ACTION_ACL_CONNECTED)

    val stateReceiver = object : BroadcastReceiver() {
        override fun onReceive(p0: Context?, intent: Intent?) {
            if (intent == null) {
                return
            }
            val action = intent!!.action
            action?.let {
                when (it) {
                    BluetoothAdapter.ACTION_STATE_CHANGED -> {
                        when (val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0)) {
                            BluetoothAdapter.STATE_OFF -> stateOff()
                            BluetoothAdapter.STATE_ON -> stateOn()  // 重啟監(jiān)聽服務
                            else -> Log.i(BluetoothClientManager.TAG, "unSupport $state")
                        }
                    }
                    BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
                        Log.i(TAG, "BluetoothDevice: ACTION_ACL_DISCONNECTED ")
                    }
                    BluetoothDevice.ACTION_ACL_CONNECTED -> {
                        Log.i(TAG, "BluetoothDevice: ACTION_ACL_CONNECTED ")
                    }
                    else -> {}
                }
            }
        }
    }
    content.registerReceiver(stateReceiver, intent)
}
二: 客戶端核心類
1:ConnectThread 用于連接服務端的線程
// 核心代碼

override fun run() {
    super.run()
    // 連接前慨亲,取消掃描
    BluetoothAdapter.getDefaultAdapter()?.cancelDiscovery()
    if (socket != null) {
        try {
            socket!!.connect()      // 5 秒會自動超時
            connectSubject.onNext(socket)
        } catch (e: IOException) {
            Log.i(TAG, "請求連接異常: $e")
            socket!!.close()
            connectSubject.onError(Error())
        }
    }
}
2:BluetoothClientManager 客戶端管理器婚瓜, 處理客戶端發(fā)起連接請求、隊列刑棵、重連
注冊系統(tǒng)藍牙開關狀態(tài):
/**
 * 注冊藍牙狀態(tài)變化
 */
private fun registerBluetoothStateEvent() {
    val intent =  IntentFilter()
    intent.addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
    intent.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
    intent.addAction(BluetoothDevice.ACTION_ACL_CONNECTED)

    val stateReceiver = object : BroadcastReceiver() {
        override fun onReceive(p0: Context?, intent: Intent?) {
            if (intent == null) {
                return
            }
            val action = intent!!.action
            action?.let {
                when (it) {
                    BluetoothAdapter.ACTION_STATE_CHANGED -> {
                        when (val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0)) {
                            BluetoothAdapter.STATE_OFF -> stateOff()
                            BluetoothAdapter.STATE_ON -> stateOn()
                            else -> Log.i(TAG, "unSupport $state")
                        }
                    }
                    BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
                        Log.i(BluetoothServerManger.TAG, "onReceive:ACTION_ACL_DISCONNECTED ")
                        handleDisconnect()
                    }
                    BluetoothDevice.ACTION_ACL_CONNECTED -> {
                        Log.i(BluetoothServerManger.TAG, "onReceive:ACTION_ACL_DISCONNECTED ")
                        connected = true
                    }
                    else -> {}
                }
            }
        }
    }
    content.registerReceiver(stateReceiver, intent)
}
客戶端掃描部分:
/**
 * 注冊掃描事件巴刻、獲取掃描結果
 */
private val receiver = object : BroadcastReceiver() {
    override fun onReceive(p0: Context?, intent: Intent?) {
        intent?.let {
            when(intent.action) {
                BluetoothDevice.ACTION_FOUND -> {
                    val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
                    device?.let {
                        val deviceHardwareAddress = device.address
                        Log.i(BluetoothClientManager.TAG, "onReceive: $deviceHardwareAddress")
                        scanSubject.onNext(Pair(BluetoothDevice.ACTION_FOUND, it))
                    }
                }
                BluetoothAdapter.ACTION_DISCOVERY_STARTED -> {
                    Log.i(BluetoothClientManager.TAG,"scan start")
                    scanSubject.onNext(Pair(BluetoothAdapter.ACTION_DISCOVERY_STARTED, null))
                }
                BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
                    unRegisterBluetoothEvent() // 取消注冊
                    Log.i(BluetoothClientManager.TAG,"scan end")
                    scanSubject.onNext(Pair(BluetoothAdapter.ACTION_DISCOVERY_FINISHED, null))
                }
                else -> {}
            }
        }
    }
}


/**
 * 開始掃描
 */
@SuppressLint("MissingPermission")
fun startDiscoverDevice() : Boolean {

    if (!bluetoothEnable()) {
        return false
    }

    if (!bluetoothIsOpen()) {
        return false
    }

    if (!havePermission()) {
        return false
    }
    registerBluetoothEvent()
    return bluetoothAdapter.startDiscovery()
}
連接部分:在ACTION_FOUND 過濾到設備,即可發(fā)起連接
// 核心代碼

/**
* 通過對象連接
*/
@Synchronized
fun connectDevice(device: BluetoothDevice, timeout: Long) : Observable<Boolean> {
    if (connecting) {
        return Observable.error(BTException("正在連接"))
    }
    connecting = true
    // 記錄連接歷史蛉签,用于重連
    historyAddress = device.address

    unRegisterBluetoothEvent()
    cancelDiscovery()

    return Observable.create<Boolean> { emitter ->
        if (connected) {
            connecting = false
            Log.i(TAG, "connectDevice: 已連接 ")
            emitter.onNext(true)
            return@create
        }
         val connectThread = ConnectThread(device)
        //  connect 會阻塞 5 秒后超時胡陪, 無需自己加超時處理
        connectThread.connectSubject
            .subscribeBy(onNext = {
                connecting = false
                startClientCommunication(it)
                emitter.onNext(true)
            }, onError = {
                connecting = false
                Log.i(BluetoothClientManager.TAG, "嘗試連接失敗")
                emitter.onError(it)
            })
        connectThread.start()
    }
}

/**
* 客戶端建立可讀寫通道
*/
private fun startClientCommunication(bs: BluetoothSocket) {
    clientCommThread = CommunicationThread(bs)
    clientCommThread!!.dataSubject
        .subscribeBy(
            onNext = {
                val valueString = it.second.toString(Charset.defaultCharset())
                var d = String(it.second)

                Log.i(BluetoothClientManager.TAG, "客戶端收到數(shù)據(jù) size: ${it.first}; $d  ${it.second} data: $valueString ")
                val source = it.second
                val taskId = (source[3].toInt() and 0xFF shl 8) or (source[2].toInt() and 0xFF)
                val ct =  currentTasks.first { t -> t.taskId == taskId }
                ct.addBuffer(source)

                if ( ct.checkComplete()) {
                    // 接受數(shù)據(jù)完成,next
                    val type = ct.type
                    ct.subscribe?.onNext(true)
                    ct.subscribe?.onComplete()
                    ct.complete()           // 完成 停止 超時記時
                    if (currentTasks.contains(ct))  currentTasks.remove(ct)
                    if (taskList.contains(ct))  taskList.remove(ct)
                    Log.i(BluetoothClientManager.TAG, "該任務已完成 task id: $taskId")
                    //下一個
                    trigger(type)
                }
            },
            onError = {
                Log.i(BluetoothClientManager.TAG, "爆發(fā)異常")
            }
        )
    clientCommThread!!.start()
}
數(shù)據(jù)處理:隊列處理碍舍,先進先出
// 核心代碼

/**
 * 數(shù)據(jù)發(fā)送
 */
@Synchronized
fun sendData(timeout: Long, message: String, type: Int = 0) : Observable<Boolean> {

    if (!bluetoothEnable()) {
        return Observable.error(BTException("藍牙不可用"))
    }

    if (!bluetoothIsOpen()) {
        return Observable.error(BTException("藍牙未開啟"))
    }

    // 發(fā)起重連
    if (!connected) {
        if (historyAddress == null) {
            return Observable.error(BTException("藍牙未連接"))
        }
        connectDeviceWithAddress(historyAddress!!)
    }
    return Observable.create<Boolean> { emitter ->
        // 存入隊列中
        taskList.add(BTTask(timeout = timeout, sourceData = message.toByteArray(), subscribe = emitter))
        if (connected) {
            trigger(type)
        }
    }
}

/**
 * 檢測是否空閑柠座, 空閑時即可發(fā)送
 *
 * type:任務類別手报,只有不同的type 才會堵塞浩销,(如所有任務都按順序,使用同一type即可)
 */
private fun trigger(type: Int) {
    if (!connected) {
        return
    }
    // 判斷是否有同類型的指令正在執(zhí)行
    if (currentTasks.any { it.type == type }) {
        return
    }
    if (taskList.size <= 0) {
        return
    }
    val btTask = taskList.first { it.type == type }
    btTask?.let {
        currentTasks.add(it)
        val cmdContent = btTask.buildCmdContent(it.sourceData!!)
        btTask.begin()
        btTask.timeoutSubject.subscribeBy(
            onError = { e ->
                // 指令超時 下一條
                btTask.subscribe?.onError(e)
                btTask.subscribe?.onComplete()
                if (currentTasks.contains(btTask))  currentTasks.remove(btTask)
                if (taskList.contains(btTask))  taskList.remove(btTask)
                Log.i(BluetoothClientManager.TAG, "該任務已超時 task id: ${btTask.taskId}")
                trigger(type)
            }
        )
        val sendResult = clientCommThread!!.write(cmdContent)
        Log.i(BluetoothClientManager.TAG, "客戶端返回發(fā)送結果: $sendResult ")
    }
}
完整代碼:

https://github.com/tlin2011/android-bluetooth-sdk.git

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末宇挫,一起剝皮案震驚了整個濱河市捧书,隨后出現(xiàn)的幾起案子吹泡,更是在濱河造成了極大的恐慌,老刑警劉巖经瓷,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爆哑,死亡現(xiàn)場離奇詭異,居然都是意外死亡舆吮,警方通過查閱死者的電腦和手機揭朝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來歪泳,“玉大人萝勤,你說我怎么就攤上這事∧派。” “怎么了敌卓?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長伶氢。 經常有香客問我趟径,道長,這世上最難降的妖魔是什么癣防? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任蜗巧,我火速辦了婚禮,結果婚禮上蕾盯,老公的妹妹穿的比我還像新娘幕屹。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布望拖。 她就那樣靜靜地躺著渺尘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪说敏。 梳的紋絲不亂的頭發(fā)上鸥跟,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天,我揣著相機與錄音盔沫,去河邊找鬼医咨。 笑死,一個胖子當著我的面吹牛架诞,可吹牛的內容都是我干的拟淮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼侈贷,長吁一口氣:“原來是場噩夢啊……” “哼惩歉!你這毒婦竟也來了等脂?” 一聲冷哼從身側響起俏蛮,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎上遥,沒想到半個月后搏屑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡粉楚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年辣恋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片模软。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡伟骨,死狀恐怖,靈堂內的尸體忽然破棺而出燃异,到底是詐尸還是另有隱情携狭,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布回俐,位于F島的核電站逛腿,受9級特大地震影響,放射性物質發(fā)生泄漏仅颇。R本人自食惡果不足惜单默,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望忘瓦。 院中可真熱鬧搁廓,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至汽摹,卻和暖如春李丰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背逼泣。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工趴泌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拉庶。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓嗜憔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親氏仗。 傳聞我的和親對象是個殘疾皇子吉捶,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內容