├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── drawable │ │ │ │ ├── ic_arrow_down.xml │ │ │ │ ├── ic_rssi.xml │ │ │ │ ├── ic_arrow_right.xml │ │ │ │ ├── ic_ble.xml │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── xml │ │ │ │ ├── backup_rules.xml │ │ │ │ └── data_extraction_rules.xml │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ ├── themes.xml │ │ │ │ └── strings.xml │ │ │ ├── menu │ │ │ │ └── top_app_bar.xml │ │ │ ├── layout │ │ │ │ ├── activity_operate.xml │ │ │ │ ├── fragment_service.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── item_service.xml │ │ │ │ ├── scan_filter.xml │ │ │ │ ├── item_ble_device.xml │ │ │ │ └── fragment_operate.xml │ │ │ ├── navigation │ │ │ │ └── operator_nav.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── values-zh │ │ │ │ └── strings.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── huyuhui │ │ │ │ └── blesample │ │ │ │ ├── Uuid.kt │ │ │ │ ├── MyApplication.kt │ │ │ │ ├── adapter │ │ │ │ ├── ScanFilterAdapter.kt │ │ │ │ ├── SharedOperateAdapter.kt │ │ │ │ ├── BleDeviceAdapter.kt │ │ │ │ └── ServiceAdapter.kt │ │ │ │ ├── AppUtils.kt │ │ │ │ ├── operate │ │ │ │ ├── OperateActivity.kt │ │ │ │ ├── ServiceFragment.kt │ │ │ │ └── SequenceNotifyOperator.kt │ │ │ │ └── ScanRecyclerView.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── huyuhui │ │ │ └── blesample │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── huyuhui │ │ └── blesample │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── library ├── .gitignore ├── consumer-rules.pro ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── huyuhui │ │ └── fastble │ │ ├── queue │ │ ├── TaskResult.kt │ │ ├── operate │ │ │ ├── SequenceBleOperator.kt │ │ │ ├── BleOperatorQueue.kt │ │ │ └── SequenceWriteOperator.kt │ │ ├── Task.kt │ │ └── Queue.kt │ │ ├── data │ │ ├── BleScanState.kt │ │ ├── BleWriteState.kt │ │ ├── BleOperatorKey.kt │ │ └── BleDevice.kt │ │ ├── common │ │ ├── BleFactory.kt │ │ ├── BluetoothChangedObserver.kt │ │ ├── TimeoutTask.kt │ │ └── BleConnectStrategy.kt │ │ ├── callback │ │ ├── BleOperateCallback.kt │ │ ├── BleScanCallback.kt │ │ ├── BleRssiCallback.kt │ │ ├── BleMtuChangedCallback.kt │ │ ├── BleReadCallback.kt │ │ ├── BleNotifyCallback.kt │ │ ├── BleIndicateCallback.kt │ │ ├── BleWriteCallback.kt │ │ └── BleGattCallback.kt │ │ ├── utils │ │ ├── BleLog.kt │ │ ├── DataUtil.kt │ │ ├── UuidUtils.kt │ │ ├── BleLruHashMap.kt │ │ └── HexUtil.kt │ │ ├── bluetooth │ │ ├── BleCharacteristicOperator.kt │ │ ├── BleReadRssiOperator.kt │ │ ├── BleMtuOperator.kt │ │ ├── BleReadOperator.kt │ │ ├── BleOperator.kt │ │ ├── BleWriteOperator.kt │ │ ├── MultipleBluetoothController.kt │ │ ├── BleIndicateOperator.kt │ │ ├── BleNotifyOperator.kt │ │ └── SplitWriter.kt │ │ ├── exception │ │ ├── BleCoroutineExceptionHandler.kt │ │ └── BleException.kt │ │ └── scan │ │ ├── BleScanRuleConfig.kt │ │ └── BleScanner.kt ├── proguard-rules.pro └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── jitpack.yml ├── .gitignore ├── settings.gradle ├── gradle.properties ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /library/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - sdk install java 17.0.10-open 3 | - sdk use java 17.0.10-open 4 | 5 | jdk: 6 | - openjdk17 -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/queue/TaskResult.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.queue 2 | 3 | data class TaskResult(val task: Task, val success: Boolean) -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/data/BleScanState.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.data 2 | 3 | internal enum class BleScanState { 4 | STATE_IDLE, STATE_SCANNING; 5 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/data/BleWriteState.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.data 2 | 3 | internal class BleWriteState { 4 | companion object { 5 | @JvmStatic 6 | val DATA_WRITE_SINGLE = 1 7 | } 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/huyuhui/blesample/Uuid.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.blesample 2 | 3 | object Uuid { 4 | const val Service = "0000ffe0-0000-1000-8000-00805F9B34FB" 5 | const val Characteristic = "0000ffe1-0000-1000-8000-00805F9B34FB" 6 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/common/BleFactory.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.common 2 | 3 | import com.huyuhui.fastble.data.BleDevice 4 | 5 | interface BleFactory { 6 | fun generateUniqueKey(bleDevice: BleDevice): String 7 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Oct 24 10:46:31 CST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/callback/BleOperateCallback.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.callback 2 | 3 | import android.bluetooth.BluetoothGattCharacteristic 4 | import com.huyuhui.fastble.data.BleDevice 5 | import com.huyuhui.fastble.exception.BleException 6 | 7 | abstract class BleOperateCallback { 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/callback/BleScanCallback.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.callback 2 | 3 | import com.huyuhui.fastble.data.BleDevice 4 | 5 | interface BleScanCallback { 6 | fun onScanStarted(success: Boolean) 7 | fun onLeScan(oldDevice: BleDevice, newDevice: BleDevice, scannedBefore: Boolean) 8 | fun onScanFinished(scanResultList: List) 9 | fun onFilter(bleDevice: BleDevice): Boolean 10 | } -------------------------------------------------------------------------------- /app/src/test/java/com/huyuhui/blesample/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.blesample 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/queue/operate/SequenceBleOperator.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.queue.operate 2 | 3 | import com.huyuhui.fastble.data.BleDevice 4 | import com.huyuhui.fastble.queue.Task 5 | import com.huyuhui.fastble.queue.TaskResult 6 | import kotlinx.coroutines.channels.Channel 7 | 8 | abstract class SequenceBleOperator(priority: Int, delay: Long) : Task(priority, delay) { 9 | abstract fun execute(bleDevice: BleDevice, channel: Channel) 10 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_down.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/callback/BleRssiCallback.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.callback 2 | 3 | import android.bluetooth.BluetoothGattCharacteristic 4 | import com.huyuhui.fastble.data.BleDevice 5 | import com.huyuhui.fastble.exception.BleException 6 | 7 | abstract class BleRssiCallback : BleOperateCallback() { 8 | 9 | abstract fun onRssiFailure(bleDevice: BleDevice, exception: BleException) 10 | 11 | abstract fun onRssiSuccess(bleDevice: BleDevice, rssi: Int) 12 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/callback/BleMtuChangedCallback.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.callback 2 | 3 | import android.bluetooth.BluetoothGattCharacteristic 4 | import com.huyuhui.fastble.data.BleDevice 5 | import com.huyuhui.fastble.exception.BleException 6 | 7 | abstract class BleMtuChangedCallback : BleOperateCallback() { 8 | 9 | abstract fun onSetMTUFailure(bleDevice: BleDevice, exception: BleException) 10 | 11 | abstract fun onMtuChanged(bleDevice: BleDevice, mtu: Int) 12 | 13 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | maven { url = 'https://jitpack.io' } 7 | } 8 | } 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | maven { url = 'https://jitpack.io' } 15 | } 16 | } 17 | rootProject.name = "FastBle" 18 | include ':app' 19 | include ':library' 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_rssi.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_right.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | #eeeeee 11 | 12 | #1DE9B6 13 | #1DE9B6 14 | #FF5722 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/callback/BleReadCallback.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.callback 2 | 3 | import android.bluetooth.BluetoothGattCharacteristic 4 | import com.huyuhui.fastble.data.BleDevice 5 | import com.huyuhui.fastble.exception.BleException 6 | 7 | abstract class BleReadCallback : BleOperateCallback() { 8 | abstract fun onReadSuccess( 9 | bleDevice: BleDevice, 10 | characteristic: BluetoothGattCharacteristic, 11 | data: ByteArray 12 | ) 13 | 14 | abstract fun onReadFailure( 15 | bleDevice: BleDevice, 16 | characteristic: BluetoothGattCharacteristic?, 17 | exception: BleException 18 | ) 19 | } -------------------------------------------------------------------------------- /app/src/main/res/menu/top_app_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/huyuhui/blesample/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.blesample 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.gtpower.myble", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/utils/BleLog.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.utils 2 | 3 | import android.util.Log 4 | 5 | @Suppress("unused") 6 | object BleLog { 7 | var isPrint = true 8 | 9 | @JvmStatic 10 | private val defaultTag = "FastBle" 11 | 12 | fun d(msg: String?) { 13 | if (isPrint && msg != null) Log.d(defaultTag, msg) 14 | } 15 | 16 | fun i(msg: String?) { 17 | if (isPrint && msg != null) Log.i(defaultTag, msg) 18 | } 19 | 20 | fun w(msg: String?) { 21 | if (isPrint && msg != null) Log.w(defaultTag, msg) 22 | } 23 | 24 | fun e(msg: String?) { 25 | if (isPrint && msg != null) Log.e(defaultTag, msg) 26 | } 27 | 28 | fun e(msg: String?, e: Throwable) { 29 | if (isPrint && msg != null) Log.e(defaultTag, msg, e) 30 | } 31 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/bluetooth/BleCharacteristicOperator.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.bluetooth 2 | 3 | import android.bluetooth.BluetoothGattCharacteristic 4 | import android.bluetooth.BluetoothGattService 5 | import com.huyuhui.fastble.data.BleOperatorKey 6 | 7 | internal abstract class BleCharacteristicOperator( 8 | bleBluetooth: BleBluetooth, 9 | timeout: Long, 10 | uuidService: String, 11 | uuidCharacteristic: String 12 | ) : BleOperator(bleBluetooth, timeout) { 13 | 14 | val mGattService: BluetoothGattService? = 15 | fromUUID(uuidService)?.let { mBluetoothGatt?.getService(it) } 16 | 17 | val mCharacteristic: BluetoothGattCharacteristic? = fromUUID(uuidCharacteristic)?.let { 18 | mGattService?.getCharacteristic(it) 19 | } 20 | 21 | val key = BleOperatorKey(uuidService, uuidCharacteristic) 22 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/queue/Task.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.queue 2 | 3 | import java.util.concurrent.atomic.AtomicLong 4 | 5 | abstract class Task(val priority: Int, var delay: Long) { 6 | companion object { 7 | private val atomic = AtomicLong(0) 8 | } 9 | 10 | var sequenceNum: Long = 0 11 | private set 12 | 13 | init { 14 | sequenceNum = atomic.getAndIncrement() 15 | } 16 | 17 | /** 18 | * 当continuous 为true的时候,等待任务完成之后(触发回调或者超时),才会进行delay任务,之后获取下一个任务 19 | * 为false的时候,会直接进行delay任务,之后获取下一个任务 20 | * @see timeout 21 | */ 22 | abstract val continuous: Boolean 23 | 24 | /** 25 | * @see continuous 26 | * 当continuous为true之后,这个设置才有效果,如果任务在时间内没有回调,直接忽略掉,进行delay任务,之后获取下一个任务 27 | * 建议给一个适当的时长,以便任务有足够时间触发回调 28 | * 如果timeout为0,则会一直等待,直到任务回调触发 29 | */ 30 | abstract val timeout: Long 31 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/callback/BleNotifyCallback.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.callback 2 | 3 | import android.bluetooth.BluetoothGattCharacteristic 4 | import com.huyuhui.fastble.data.BleDevice 5 | import com.huyuhui.fastble.exception.BleException 6 | 7 | abstract class BleNotifyCallback : BleOperateCallback() { 8 | abstract fun onNotifySuccess(bleDevice: BleDevice, characteristic: BluetoothGattCharacteristic) 9 | 10 | abstract fun onNotifyFailure( 11 | bleDevice: BleDevice, 12 | characteristic: BluetoothGattCharacteristic?, 13 | exception: BleException 14 | ) 15 | 16 | open fun onNotifyCancel(bleDevice: BleDevice, characteristic: BluetoothGattCharacteristic){} 17 | abstract fun onCharacteristicChanged( 18 | bleDevice: BleDevice, 19 | characteristic: BluetoothGattCharacteristic, 20 | data: ByteArray 21 | ) 22 | 23 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/data/BleOperatorKey.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.data 2 | 3 | class BleOperatorKey(uuidService: String, uuidCharacteristic: String) { 4 | private val normalizedServiceUuid = uuidService.lowercase() 5 | private val normalizedCharacteristicUuid = uuidCharacteristic.lowercase() 6 | override fun equals(other: Any?): Boolean { 7 | if (this === other) return true 8 | if (javaClass != other?.javaClass) return false 9 | 10 | other as BleOperatorKey 11 | 12 | if (normalizedServiceUuid != other.normalizedServiceUuid) return false 13 | if (normalizedCharacteristicUuid != other.normalizedCharacteristicUuid) return false 14 | 15 | return true 16 | } 17 | 18 | override fun hashCode(): Int { 19 | var result = normalizedServiceUuid.hashCode() 20 | result = 31 * result + normalizedCharacteristicUuid.hashCode() 21 | return result 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/callback/BleIndicateCallback.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.callback 2 | 3 | import android.bluetooth.BluetoothGattCharacteristic 4 | import com.huyuhui.fastble.data.BleDevice 5 | import com.huyuhui.fastble.exception.BleException 6 | 7 | abstract class BleIndicateCallback : BleOperateCallback() { 8 | abstract fun onIndicateSuccess( 9 | bleDevice: BleDevice, 10 | characteristic: BluetoothGattCharacteristic 11 | ) 12 | 13 | abstract fun onIndicateFailure( 14 | bleDevice: BleDevice, 15 | characteristic: BluetoothGattCharacteristic?, 16 | exception: BleException 17 | ) 18 | 19 | open fun onIndicateCancel( 20 | bleDevice: BleDevice, 21 | characteristic: BluetoothGattCharacteristic 22 | ){} 23 | 24 | abstract fun onCharacteristicChanged( 25 | bleDevice: BleDevice, 26 | characteristic: BluetoothGattCharacteristic, 27 | data: ByteArray? 28 | ) 29 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_ble.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_operate.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/queue/operate/BleOperatorQueue.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.queue.operate 2 | 3 | import com.huyuhui.fastble.bluetooth.BleBluetooth 4 | import com.huyuhui.fastble.exception.BleCoroutineExceptionHandler 5 | import com.huyuhui.fastble.queue.Queue 6 | import com.huyuhui.fastble.queue.TaskResult 7 | import com.huyuhui.fastble.utils.BleLog 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.SupervisorJob 10 | import kotlinx.coroutines.channels.Channel 11 | import kotlinx.coroutines.job 12 | 13 | internal class BleOperatorQueue(private val bleBluetooth: BleBluetooth) : 14 | Queue(), 15 | CoroutineScope by CoroutineScope( 16 | SupervisorJob(bleBluetooth.coroutineContext.job) + BleCoroutineExceptionHandler({ _, throwable -> 17 | BleLog.e("Ble operator queue:a coroutine error has occurred ${throwable.message}") 18 | }) 19 | ) { 20 | override fun execute(task: SequenceBleOperator, channel: Channel) { 21 | task.execute(bleBluetooth.bleDevice, channel) 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/huyuhui/blesample/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.blesample 2 | 3 | import android.app.Application 4 | import com.huyuhui.fastble.BleManager 5 | import com.huyuhui.fastble.common.BleConnectStrategy 6 | import com.huyuhui.fastble.common.BleFactory 7 | import com.huyuhui.fastble.data.BleDevice 8 | 9 | class MyApplication : Application() { 10 | override fun onCreate() { 11 | super.onCreate() 12 | BleManager.apply { 13 | enableLog(true) 14 | maxConnectCount = 5 15 | operateTimeout = 2000 16 | splitWriteNum = 20 17 | bleConnectStrategy = BleConnectStrategy.Builder().setConnectOverTime(10000) 18 | .setConnectBackpressureStrategy(BleConnectStrategy.CONNECT_BACKPRESSURE_DROP) 19 | .setReConnectCount(1).setReConnectInterval(2000).build() 20 | bleFactory = object :BleFactory{ 21 | override fun generateUniqueKey(bleDevice: BleDevice): String { 22 | return bleDevice.mac 23 | } 24 | } 25 | }.init(this) 26 | } 27 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/callback/BleWriteCallback.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.callback 2 | 3 | import android.bluetooth.BluetoothGattCharacteristic 4 | import com.huyuhui.fastble.data.BleDevice 5 | import com.huyuhui.fastble.data.BleWriteState 6 | import com.huyuhui.fastble.exception.BleException 7 | 8 | abstract class BleWriteCallback : BleOperateCallback() { 9 | 10 | abstract fun onWriteSuccess( 11 | bleDevice: BleDevice, 12 | characteristic: BluetoothGattCharacteristic, 13 | current: Int = BleWriteState.DATA_WRITE_SINGLE, 14 | total: Int = BleWriteState.DATA_WRITE_SINGLE, 15 | justWrite: ByteArray, 16 | data: ByteArray = justWrite 17 | ) 18 | 19 | abstract fun onWriteFailure( 20 | bleDevice: BleDevice, 21 | characteristic: BluetoothGattCharacteristic?, 22 | exception: BleException, 23 | current: Int = BleWriteState.DATA_WRITE_SINGLE, 24 | total: Int = BleWriteState.DATA_WRITE_SINGLE, 25 | justWrite: ByteArray?, 26 | data: ByteArray? = justWrite, 27 | isTotalFail: Boolean = true 28 | ) 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/huyuhui/blesample/adapter/ScanFilterAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.blesample.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.huyuhui.blesample.databinding.ScanFilterBinding 7 | 8 | class ScanFilterAdapter : RecyclerView.Adapter() { 9 | var scanFilterBinding: ScanFilterBinding? = null 10 | private set 11 | 12 | class ScanViewHolder(binding: ScanFilterBinding) : 13 | RecyclerView.ViewHolder(binding.root) 14 | 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScanViewHolder { 16 | return ScanViewHolder( 17 | ScanFilterBinding.inflate( 18 | LayoutInflater.from(parent.context), 19 | parent, 20 | false 21 | ).apply { 22 | scanFilterBinding = this 23 | }) 24 | } 25 | 26 | override fun getItemCount(): Int { 27 | return 1 28 | } 29 | 30 | override fun onBindViewHolder(holder: ScanViewHolder, position: Int) { 31 | 32 | } 33 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/utils/DataUtil.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.utils 2 | 3 | import java.util.LinkedList 4 | import java.util.Queue 5 | 6 | object DataUtil { 7 | fun splitPacketForByte(data: ByteArray?, length: Int): Queue { 8 | val dataInfoQueue: Queue = LinkedList() 9 | if (data != null) { 10 | var index = 0 11 | do { 12 | val surplusData = ByteArray(data.size - index) 13 | var currentData: ByteArray 14 | System.arraycopy(data, index, surplusData, 0, data.size - index) 15 | if (surplusData.size <= length) { 16 | currentData = ByteArray(surplusData.size) 17 | System.arraycopy(surplusData, 0, currentData, 0, surplusData.size) 18 | index += surplusData.size 19 | } else { 20 | currentData = ByteArray(length) 21 | System.arraycopy(data, index, currentData, 0, length) 22 | index += length 23 | } 24 | dataInfoQueue.offer(currentData) 25 | } while (index < data.size) 26 | } 27 | return dataInfoQueue 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_service.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 24 | 25 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/navigation/operator_nav.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 16 | 19 | 22 | 23 | 24 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/huyuhui/blesample/adapter/SharedOperateAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.blesample.adapter 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | import android.view.ViewGroup.LayoutParams 6 | import android.widget.TextView 7 | import androidx.recyclerview.widget.LinearLayoutManager 8 | import androidx.recyclerview.widget.RecyclerView 9 | 10 | class SharedOperateAdapter(private val data: List) : 11 | RecyclerView.Adapter() { 12 | 13 | override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { 14 | super.onAttachedToRecyclerView(recyclerView) 15 | recyclerView.layoutManager = LinearLayoutManager(recyclerView.context) 16 | } 17 | class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) 18 | 19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { 20 | return ItemViewHolder(TextView(parent.context).apply { 21 | layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) 22 | }) 23 | } 24 | 25 | override fun getItemCount(): Int { 26 | return data.size 27 | } 28 | 29 | override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { 30 | (holder.itemView as TextView).text = data[position] 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 17 | 18 | 27 | 28 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/exception/BleCoroutineExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.exception 2 | 3 | import kotlinx.coroutines.CoroutineExceptionHandler 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.MainScope 6 | import kotlin.coroutines.CoroutineContext 7 | 8 | // 扩展 MainScope,返回一个包含 exceptionHandler 的作用域 9 | fun CoroutineScope.withExceptionHandler(handler: CoroutineExceptionHandler): CoroutineScope { 10 | return CoroutineScope(coroutineContext + handler) 11 | } 12 | 13 | @Suppress("FunctionName") 14 | fun BleMainScope(handler: (CoroutineContext, Throwable) -> Unit): CoroutineScope = 15 | MainScope().withExceptionHandler(BleCoroutineExceptionHandler(handler)) 16 | 17 | @Suppress("FunctionName") 18 | inline fun BleCoroutineExceptionHandler(crossinline handler: (CoroutineContext, Throwable) -> Unit): CoroutineExceptionHandler = 19 | CoroutineExceptionHandler { coroutineContext, exception -> 20 | println("Caught exception: ${exception.message}") 21 | println("Stack trace:") 22 | exception.stackTrace.filter { it.className.startsWith("com.huyuhui.fastble") } 23 | .forEach { stackTraceElement -> 24 | println(" at $stackTraceElement") 25 | } 26 | // 打印完整的堆栈跟踪 27 | exception.stackTrace.forEach { stackTraceElement -> 28 | println(" at $stackTraceElement") 29 | } 30 | handler(coroutineContext, exception) 31 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | 25 | android.enableJetifier = true -------------------------------------------------------------------------------- /library/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | id("com.android.library") 5 | id("org.jetbrains.kotlin.android") 6 | id("maven-publish") 7 | } 8 | val VERSION_NAME = "1.0" 9 | val GROUP_ID = "com.github.kanshenmekan" 10 | val ARTIFACT_ID = "FastBle" 11 | android { 12 | namespace = "com.huyuhui.fastble" 13 | compileSdk = 35 14 | 15 | defaultConfig { 16 | minSdk = 21 17 | } 18 | compileOptions { 19 | sourceCompatibility = JavaVersion.VERSION_1_8 20 | targetCompatibility = JavaVersion.VERSION_1_8 21 | } 22 | kotlin { 23 | compilerOptions { 24 | jvmTarget.set(JvmTarget.JVM_1_8) 25 | } 26 | } 27 | packaging { 28 | // 剔除这个包下的所有文件(不会移除签名信息) 29 | resources.excludes.add("META-INF/*******") 30 | } 31 | publishing { 32 | singleVariant("release") { 33 | withSourcesJar() 34 | withJavadocJar() 35 | } 36 | } 37 | } 38 | 39 | afterEvaluate { 40 | publishing { 41 | publications { 42 | create("release") { 43 | groupId = GROUP_ID 44 | artifactId = ARTIFACT_ID 45 | version = VERSION_NAME 46 | from(components["release"]) 47 | } 48 | } 49 | } 50 | } 51 | 52 | dependencies { 53 | implementation("androidx.core:core-ktx:1.16.0") 54 | implementation("androidx.appcompat:appcompat:1.7.1") 55 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/callback/BleGattCallback.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.callback 2 | 3 | import android.bluetooth.BluetoothGatt 4 | import android.bluetooth.BluetoothGattCallback 5 | import com.huyuhui.fastble.data.BleDevice 6 | import com.huyuhui.fastble.exception.BleException 7 | 8 | abstract class BleGattCallback : BluetoothGattCallback() { 9 | 10 | abstract fun onStartConnect(bleDevice: BleDevice) 11 | 12 | abstract fun onConnectFail(bleDevice: BleDevice, exception: BleException) 13 | 14 | /** 15 | * @param skip true表示 当前发起的连接,设备已经连接,或者该连接被新发起的连接覆盖 false 则表示手动取消了该次连接 16 | */ 17 | open fun onConnectCancel(bleDevice: BleDevice, skip: Boolean) {} 18 | abstract fun onConnectSuccess(bleDevice: BleDevice, gatt: BluetoothGatt?, status: Int) 19 | 20 | abstract fun onDisConnected( 21 | isActiveDisConnected: Boolean, 22 | device: BleDevice, 23 | gatt: BluetoothGatt?, 24 | status: Int 25 | ) 26 | 27 | open fun onServicesDiscovered(bleDevice: BleDevice, gatt: BluetoothGatt?, status: Int) { 28 | 29 | } 30 | 31 | open fun onPhyUpdate( 32 | bleDevice: BleDevice, 33 | gatt: BluetoothGatt?, 34 | txPhy: Int, 35 | rxPhy: Int, 36 | status: Int 37 | ) { 38 | 39 | } 40 | 41 | open fun onPhyRead( 42 | bleDevice: BleDevice, 43 | gatt: BluetoothGatt?, 44 | txPhy: Int, 45 | rxPhy: Int, 46 | status: Int 47 | ) { 48 | 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/utils/UuidUtils.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.utils 2 | 3 | import android.annotation.SuppressLint 4 | 5 | @Suppress("unused") 6 | object UuidUtils { 7 | private const val BASE_UUID_REGEX = 8 | "0000([0-9a-f][0-9a-f][0-9a-f][0-9a-f])-0000-1000-8000-00805f9b34fb" 9 | private const val BASE_UUID = "0000xxxx-0000-1000-8000-00805F9B34FB" 10 | 11 | @SuppressLint("PrivateApi") 12 | fun isBaseUUID(uuid: String): Boolean { 13 | return uuid.lowercase() 14 | .matches(Regex("0000([0-9a-f][0-9a-f][0-9a-f][0-9a-f])-0000-1000-8000-00805f9b34fb")) 15 | } 16 | 17 | fun is16UUID(uuid: String): Boolean { 18 | return uuid.matches(Regex("""^[0-9a-fA-F]{4}$""")) 19 | } 20 | 21 | fun isUUID(str: String): Boolean { 22 | val uuidPattern = Regex( 23 | """^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$""" 24 | ) 25 | return uuidPattern.matches(str) 26 | } 27 | 28 | fun uuid128To16(uuid: String, lowerCase: Boolean = true): String? { 29 | return if (isBaseUUID(uuid)) { 30 | if (lowerCase) uuid.substring(4, 8).lowercase() 31 | else uuid.substring(4, 8).uppercase() 32 | } else null 33 | } 34 | 35 | fun uuid16To128(uuid: String, lowerCase: Boolean = true): String? { 36 | return if (is16UUID(uuid)) { 37 | if (lowerCase) BASE_UUID.replaceRange(4, 8, uuid) 38 | .lowercase() else BASE_UUID.replaceRange(4, 8, uuid).uppercase() 39 | } else null 40 | } 41 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/bluetooth/BleReadRssiOperator.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.bluetooth 2 | 3 | import android.annotation.SuppressLint 4 | import com.huyuhui.fastble.callback.BleRssiCallback 5 | import com.huyuhui.fastble.common.TimeoutTask 6 | import com.huyuhui.fastble.exception.BleException 7 | 8 | @SuppressLint("MissingPermission") 9 | internal class BleReadRssiOperator( 10 | bleBluetooth: BleBluetooth, 11 | timeout: Long 12 | ) : BleOperator(bleBluetooth, timeout) { 13 | var bleRssiCallback: BleRssiCallback? = null 14 | private set 15 | 16 | /** 17 | * rssi 18 | */ 19 | fun readRemoteRssi(bleRssiCallback: BleRssiCallback?) { 20 | if (mBluetoothGatt == null) { 21 | bleRssiCallback?.onRssiFailure( 22 | bleDevice, 23 | BleException.OtherException(BleException.GATT_NULL, "gatt is null") 24 | ) 25 | } else { 26 | timeOutTask.start(this) 27 | this.bleRssiCallback = bleRssiCallback 28 | bleBluetooth.setRssiOperator(this) 29 | if (!mBluetoothGatt!!.readRemoteRssi()) { 30 | removeTimeOut() 31 | bleRssiCallback?.onRssiFailure( 32 | bleDevice, 33 | BleException.OtherException(message = "gatt readRemoteRssi fail") 34 | ) 35 | } 36 | } 37 | } 38 | 39 | override fun onTimeout( 40 | task: TimeoutTask, 41 | e: Throwable?, 42 | isActive: Boolean 43 | ) { 44 | bleRssiCallback?.onRssiFailure(bleDevice, BleException.TimeoutException()) 45 | } 46 | 47 | override fun destroy() { 48 | super.destroy() 49 | bleRssiCallback = null 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/bluetooth/BleMtuOperator.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.bluetooth 2 | 3 | import android.annotation.SuppressLint 4 | import com.huyuhui.fastble.callback.BleMtuChangedCallback 5 | import com.huyuhui.fastble.common.TimeoutTask 6 | import com.huyuhui.fastble.exception.BleException 7 | 8 | @SuppressLint("MissingPermission") 9 | internal class BleMtuOperator( 10 | bleBluetooth: BleBluetooth, 11 | timeout: Long 12 | ) : BleOperator(bleBluetooth, timeout) { 13 | 14 | var bleMtuChangedCallback: BleMtuChangedCallback? = null 15 | private set 16 | 17 | /** 18 | * set mtu 19 | */ 20 | fun setMtu(requiredMtu: Int, bleMtuChangedCallback: BleMtuChangedCallback?) { 21 | if (mBluetoothGatt == null) { 22 | bleMtuChangedCallback?.onSetMTUFailure( 23 | bleDevice, 24 | BleException.OtherException(message = "gatt requestMtu fail") 25 | ) 26 | } else { 27 | timeOutTask.start(this) 28 | this.bleMtuChangedCallback = bleMtuChangedCallback 29 | bleBluetooth.setMtuOperator(this) 30 | if (!mBluetoothGatt!!.requestMtu(requiredMtu)) { 31 | removeTimeOut() 32 | bleMtuChangedCallback?.onSetMTUFailure( 33 | bleDevice, 34 | BleException.OtherException(message = "gatt requestMtu fail") 35 | ) 36 | } 37 | } 38 | } 39 | 40 | override fun onTimeout( 41 | task: TimeoutTask, 42 | e: Throwable?, 43 | isActive: Boolean 44 | ) { 45 | bleMtuChangedCallback?.onSetMTUFailure(bleDevice, BleException.TimeoutException()) 46 | } 47 | 48 | override fun destroy() { 49 | super.destroy() 50 | bleMtuChangedCallback = null 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_service.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 25 | 26 | 36 | 37 | 45 | -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/utils/BleLruHashMap.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.utils 2 | 3 | import com.huyuhui.fastble.bluetooth.BleBluetooth 4 | import kotlin.math.ceil 5 | 6 | internal class BleLruHashMap(private val maxSize: Int) : LinkedHashMap( 7 | ceil(maxSize / 0.75).toInt() + 1, 0.75f, true 8 | ) { 9 | 10 | // 内部锁对象,保护所有操作的线程安全 11 | private val lock = Any() 12 | 13 | override fun removeEldestEntry(eldest: MutableMap.MutableEntry): Boolean { 14 | if (size > maxSize && eldest.value is BleBluetooth) { 15 | BleLog.w("The number of connections has surpassed the maximum limit.") 16 | (eldest.value as BleBluetooth).disconnect() 17 | } 18 | return size > maxSize 19 | } 20 | 21 | override val size: Int 22 | get() = synchronized(lock) { super.size } 23 | 24 | override fun isEmpty(): Boolean = synchronized(lock) { super.isEmpty() } 25 | 26 | override fun containsKey(key: K): Boolean = synchronized(lock) { super.containsKey(key) } 27 | 28 | override fun containsValue(value: V): Boolean = synchronized(lock) { super.containsValue(value) } 29 | 30 | override fun get(key: K): V? = synchronized(lock) { super.get(key) } 31 | 32 | override fun put(key: K, value: V): V? = synchronized(lock) { super.put(key, value) } 33 | 34 | override fun remove(key: K): V? = synchronized(lock) { super.remove(key) } 35 | 36 | override fun putAll(from: Map) = synchronized(lock) { super.putAll(from) } 37 | 38 | override fun clear() = synchronized(lock) { super.clear() } 39 | 40 | override fun toString(): String = synchronized(lock) { 41 | val sb = StringBuilder() 42 | for ((key, value) in entries) { 43 | sb.append(String.format("%s:%s ", key, value)) 44 | } 45 | sb.toString() 46 | } 47 | 48 | override val keys: MutableSet 49 | get() = synchronized(lock) { super.keys } 50 | // 新增:获取不可变的键集合(避免外部迭代修改) 51 | 52 | override val values: MutableCollection 53 | get() = synchronized(lock) { super.values } 54 | 55 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 12 | 15 | 19 | 23 | 27 | 28 | 29 | 30 | 41 | 44 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/bluetooth/BleReadOperator.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.bluetooth 2 | 3 | import android.annotation.SuppressLint 4 | import android.bluetooth.BluetoothGattCharacteristic 5 | import com.huyuhui.fastble.callback.BleReadCallback 6 | import com.huyuhui.fastble.common.TimeoutTask 7 | import com.huyuhui.fastble.exception.BleException 8 | 9 | @SuppressLint("MissingPermission") 10 | internal class BleReadOperator( 11 | bleBluetooth: BleBluetooth, 12 | timeout: Long, 13 | uuidService: String, 14 | uuidCharacteristic: String 15 | ) : BleCharacteristicOperator(bleBluetooth, timeout, uuidService, uuidCharacteristic) { 16 | var bleReadCallback: BleReadCallback? = null 17 | private set 18 | 19 | /** 20 | * read 21 | */ 22 | fun readCharacteristic(bleReadCallback: BleReadCallback?) { 23 | if (mCharacteristic != null 24 | && mCharacteristic.properties and BluetoothGattCharacteristic.PROPERTY_READ > 0 25 | ) { 26 | this.bleReadCallback = bleReadCallback 27 | timeOutTask.start(this) 28 | bleBluetooth.addReadOperator(key, this) 29 | if (!mBluetoothGatt!!.readCharacteristic(mCharacteristic)) { 30 | removeTimeOut() 31 | bleReadCallback?.onReadFailure( 32 | bleDevice, 33 | mCharacteristic, 34 | BleException.OtherException( 35 | BleException.CHARACTERISTIC_ERROR, 36 | "gatt readCharacteristic fail" 37 | ) 38 | ) 39 | } 40 | } else { 41 | bleReadCallback?.onReadFailure( 42 | bleDevice, 43 | mCharacteristic, 44 | BleException.OtherException( 45 | BleException.CHARACTERISTIC_NOT_SUPPORT, 46 | "this characteristic not support read!" 47 | ) 48 | ) 49 | } 50 | } 51 | 52 | override fun onTimeout( 53 | task: TimeoutTask, 54 | e: Throwable?, 55 | isActive: Boolean 56 | ) { 57 | bleReadCallback?.onReadFailure(bleDevice, mCharacteristic, BleException.TimeoutException()) 58 | } 59 | 60 | override fun destroy() { 61 | super.destroy() 62 | bleReadCallback = null 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/res/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FastBle 3 | 搜索 4 | 停止 5 | 6 | 提示 7 | 当前手机需要打开定位服务才能搜索蓝牙 8 | 去设置 9 | 取消 10 | 11 | 展开搜索设置 12 | 收起搜索设置 13 | 连接失败 14 | 连接断开 15 | 连接中断 16 | 请先打开蓝牙 17 | 选择操作 18 | Characteristic 19 | 数据: 20 | Read 21 | Write 22 | Write No Response 23 | Write Signed 24 | indicate 25 | notification 26 | 打开通知 27 | 关闭通知 28 | Service列表 29 | Characteristic列表 30 | 控制 31 | 设置广播名: 32 | MAC: 33 | Service 34 | Service type (Primary service) 35 | Secondary type (Secondary service) 36 | Service type (main service) 37 | 在下面设置过滤条件, 可以为空,多个数据用英文逗号隔开 38 | 输入广播名 39 | 输入设备MAC 40 | 输入设备UUID,16位或者128位 41 | 输入16进制格式的数据 42 | 连接 43 | 已连接 44 | 断开 45 | Enter 46 | 操作 47 | 蓝牙权限被禁止 48 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace = 'com.huyuhui.blesample' 8 | compileSdk = 35 9 | 10 | defaultConfig { 11 | applicationId "com.huyuhui.blesample" 12 | minSdk = 23 13 | targetSdk = 35 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary = true 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_17 31 | targetCompatibility JavaVersion.VERSION_17 32 | } 33 | kotlin { 34 | jvmToolchain(17) // 设置 JVM 17 35 | } 36 | buildFeatures { 37 | viewBinding = true 38 | } 39 | packagingOptions { 40 | resources { 41 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 42 | } 43 | } 44 | } 45 | 46 | dependencies { 47 | 48 | implementation 'androidx.core:core-ktx:1.13.1' 49 | implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.22') 50 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7' 51 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 52 | testImplementation 'junit:junit:4.13.2' 53 | androidTestImplementation 'androidx.test.ext:junit:1.2.1' 54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' 55 | implementation "androidx.activity:activity-ktx:1.9.3" 56 | implementation 'androidx.appcompat:appcompat:1.7.0' 57 | implementation 'com.google.android.material:material:1.12.0' 58 | implementation 'com.github.getActivity:XXPermissions:18.3' 59 | implementation project(':library') 60 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14' 61 | implementation 'androidx.recyclerview:recyclerview:1.3.2' 62 | def nav_version = "2.8.3" 63 | implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" 64 | implementation "androidx.navigation:navigation-ui-ktx:$nav_version" 65 | implementation 'com.github.li-xiaojun:XPopup:2.10.0' 66 | // implementation 'com.google.guava:guava:30.1-jre' 67 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/bluetooth/BleOperator.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.bluetooth 2 | 3 | import android.annotation.SuppressLint 4 | import android.bluetooth.BluetoothGatt 5 | import com.huyuhui.fastble.common.TimeoutTask 6 | import com.huyuhui.fastble.data.BleDevice 7 | import com.huyuhui.fastble.exception.BleCoroutineExceptionHandler 8 | import com.huyuhui.fastble.utils.BleLog 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.SupervisorJob 12 | import kotlinx.coroutines.cancel 13 | import kotlinx.coroutines.job 14 | import java.util.UUID 15 | 16 | @SuppressLint("MissingPermission") 17 | internal abstract class BleOperator( 18 | protected val bleBluetooth: BleBluetooth, 19 | val timeout: Long 20 | ) : 21 | CoroutineScope by CoroutineScope( 22 | SupervisorJob(bleBluetooth.coroutineContext.job) + Dispatchers.Main 23 | + BleCoroutineExceptionHandler { _, throwable -> 24 | BleLog.e( 25 | "Bluetooth operation: a coroutine error has occurred. ${throwable.message}\n " + 26 | "Device:${bleBluetooth.bleDevice} \n" 27 | ) 28 | }) { 29 | companion object { 30 | @JvmStatic 31 | val UUID_CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR = 32 | "00002902-0000-1000-8000-00805f9b34fb" 33 | 34 | 35 | const val WRITE_TYPE_DEFAULT = -1 36 | } 37 | 38 | /** 39 | * 操作的数据 40 | */ 41 | val bleDevice: BleDevice 42 | get() = bleBluetooth.bleDevice 43 | val mBluetoothGatt: BluetoothGatt? 44 | get() = bleBluetooth.bluetoothGatt 45 | 46 | abstract fun onTimeout(task: TimeoutTask, e: Throwable?, isActive: Boolean) 47 | 48 | protected val timeOutTask = TimeoutTask( 49 | timeout, object : TimeoutTask.OnResultCallBack { 50 | override fun onError(task: TimeoutTask, e: Throwable?, isActive: Boolean) { 51 | super.onError(task, e, isActive) 52 | onTimeout(task, e, isActive) 53 | } 54 | } 55 | ) 56 | 57 | protected fun fromUUID(uuid: String): UUID? { 58 | return try { 59 | UUID.fromString(uuid) 60 | } catch (_: IllegalArgumentException) { 61 | null 62 | } 63 | 64 | } 65 | 66 | fun hasTask(): Boolean { 67 | return timeOutTask.hasTask() 68 | } 69 | 70 | fun removeTimeOut() { 71 | timeOutTask.success() 72 | } 73 | 74 | open fun destroy() { 75 | timeOutTask.onTimeoutResultCallBack = null 76 | cancel() 77 | } 78 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/common/BluetoothChangedObserver.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.common 2 | 3 | import android.bluetooth.BluetoothAdapter 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.IntentFilter 8 | import com.huyuhui.fastble.BleManager 9 | import com.huyuhui.fastble.scan.BleScanner 10 | import java.lang.ref.WeakReference 11 | 12 | 13 | class BluetoothChangedObserver { 14 | private var mBleReceiver: BleReceiver? = null 15 | var bleStatusCallback: BleStatusCallback? = null 16 | 17 | interface BleStatusCallback { 18 | fun onStateOn() 19 | fun onStateTurningOn() 20 | fun onStateOff() 21 | fun onStateTurningOff() 22 | } 23 | 24 | fun registerReceiver(context: Context) { 25 | mBleReceiver = BleReceiver(this) 26 | val filter = IntentFilter() 27 | filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED) 28 | context.registerReceiver(mBleReceiver, filter) 29 | } 30 | 31 | fun unregisterReceiver(context: Context) { 32 | if (mBleReceiver != null) { 33 | try { 34 | context.unregisterReceiver(mBleReceiver) 35 | bleStatusCallback = null 36 | } catch (e: Exception) { 37 | e.printStackTrace() 38 | } 39 | } 40 | } 41 | 42 | class BleReceiver(bluetoothChangedObserver: BluetoothChangedObserver) : BroadcastReceiver() { 43 | private var mObserverWeakReference: WeakReference = WeakReference(bluetoothChangedObserver) 44 | 45 | override fun onReceive(context: Context?, intent: Intent?) { 46 | if (intent?.action == BluetoothAdapter.ACTION_STATE_CHANGED) { 47 | val observer = mObserverWeakReference.get() 48 | when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) { 49 | BluetoothAdapter.STATE_OFF -> { 50 | observer?.bleStatusCallback?.onStateOff() 51 | BleScanner.stopLeScan() 52 | BleManager.multipleBluetoothController.onBleOff() 53 | } 54 | 55 | BluetoothAdapter.STATE_TURNING_OFF -> { 56 | observer?.bleStatusCallback?.onStateTurningOff() 57 | } 58 | 59 | BluetoothAdapter.STATE_ON -> { 60 | observer?.bleStatusCallback?.onStateOn() 61 | } 62 | 63 | BluetoothAdapter.STATE_TURNING_ON -> { 64 | observer?.bleStatusCallback?.onStateTurningOn() 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/huyuhui/blesample/AppUtils.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.blesample 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.* 5 | import android.bluetooth.BluetoothAdapter 6 | import android.bluetooth.BluetoothManager 7 | import android.content.Context 8 | import android.content.Intent 9 | import android.content.pm.PackageManager 10 | import android.location.LocationManager 11 | import android.provider.Settings 12 | import androidx.activity.ComponentActivity 13 | import androidx.activity.result.ActivityResultLauncher 14 | import com.hjq.permissions.Permission 15 | import com.hjq.permissions.XXPermissions 16 | 17 | 18 | object AppUtils { 19 | @SuppressLint("MissingPermission") 20 | fun enableBluetooth( 21 | activity: ComponentActivity, 22 | launcher: ActivityResultLauncher 23 | ) { 24 | val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) 25 | if (XXPermissions.isGranted(activity, *Permission.Group.BLUETOOTH)) { 26 | launcher.launch(intent) 27 | } 28 | } 29 | 30 | fun isSupportBle(context: Context?): Boolean { 31 | if (context == null || !context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { 32 | return false 33 | } 34 | context.getSystemService(Context.BLUETOOTH_SERVICE)?.let { 35 | return (it as BluetoothManager).adapter != null 36 | } 37 | return false 38 | } 39 | 40 | fun isBleEnable(context: Context): Boolean { 41 | if (!isSupportBle(context)) { 42 | return false 43 | } 44 | val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager 45 | return manager.adapter.isEnabled 46 | } 47 | 48 | fun isSupportGPS(context: Context): Boolean { 49 | return context.getSystemService(Context.LOCATION_SERVICE) != null 50 | } 51 | 52 | /** 53 | * 判断GPS是否开启,GPS或者AGPS开启一个就认为是开启的 54 | * 55 | * @param context 56 | * @return true 表示开启 57 | */ 58 | fun isOPenGPS(context: Context): Boolean { 59 | if (!isSupportGPS(context)) return false 60 | val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager 61 | // 通过GPS卫星定位,定位级别可以精确到街(通过24颗卫星定位,在室外和空旷的地方定位准确、速度快) 62 | val gps = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) 63 | // 通过WLAN或移动网络(3G/2G)确定的位置(也称作AGPS,辅助GPS定位。主要用于在室内或遮盖物(建筑群或茂密的深林等)密集的地方定位) 64 | val network = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) 65 | return gps || network 66 | } 67 | 68 | fun openGPS(activity: Activity) { 69 | val intent = 70 | Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 71 | activity.startActivity(intent) 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/scan_filter.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 20 | 21 | 26 | 27 | 32 | 33 | 34 | 39 | 40 | 45 | 46 | 47 | 53 | 54 | 59 | 60 | 67 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FastBle 3 | Scan 4 | Stop 5 | 6 | Prompt 7 | Current mobile phone scanning Bluetooth needs to open the positioning function 8 | Go to settings 9 | Cancel 10 | 11 | Expand search settings 12 | Retrieve search settings 13 | connection failed 14 | disconnect 15 | Connection Broken 16 | Please turn on Bluetooth first 17 | Select operation type 18 | Characteristic 19 | Data changes: 20 | Read 21 | Write 22 | Write No Response 23 | Write Signed 24 | indicate 25 | notification 26 | Open notification 27 | Close notification 28 | Service list 29 | Characteristic list 30 | Console 31 | Device broadcast name: 32 | MAC: 33 | Service 34 | Service type (Primary service) 35 | Secondary type (Secondary service) 36 | Service type (main service) 37 | Below, you can configure the conditions that you need to scan the device, which can be empty,separated by English commas 38 | Enter a broadcast name for Bluetooth devices 39 | Enter Bluetooth device MAC 40 | Enter Bluetooth device UUID,16 bits or 128 bits 41 | Please enter the HEX format command 42 | Connect 43 | Connected 44 | Disconnect 45 | Enter 46 | Detail 47 | The Bluetooth permission is not obtained 48 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/exception/BleException.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.exception 2 | 3 | import android.bluetooth.BluetoothGatt 4 | 5 | sealed class BleException(open val code: Int, message: String) : Throwable(message) { 6 | companion object { 7 | @JvmStatic 8 | val ERROR_CODE_TIMEOUT = 100 9 | 10 | @JvmStatic 11 | val ERROR_CODE_GATT = 101 12 | 13 | @JvmStatic 14 | val ERROR_CODE_OTHER = 102 15 | 16 | @JvmStatic 17 | val NOT_SUPPORT_BLE = 2005 18 | 19 | @JvmStatic 20 | val BLUETOOTH_NOT_ENABLED = 2006 21 | 22 | @JvmStatic 23 | val DEVICE_NULL = 2007 24 | 25 | @JvmStatic 26 | val DEVICE_NOT_CONNECT = 2008 27 | 28 | @JvmStatic 29 | val DATA_NULL = 2009 30 | 31 | @JvmStatic 32 | val GATT_NULL = 2010 33 | 34 | @JvmStatic 35 | val CHARACTERISTIC_NOT_SUPPORT = 2011 36 | 37 | @JvmStatic 38 | val CHARACTERISTIC_ERROR = 2012 39 | 40 | @JvmStatic 41 | val DESCRIPTOR_NULL = 2013 42 | 43 | @JvmStatic 44 | val DESCRIPTOR_ERROR = 2014 45 | 46 | @JvmStatic 47 | val COROUTINE_SCOPE_CANCELLED = 2015 48 | 49 | // @JvmStatic 50 | // val DEVICE_HAS_CONNECTED = 2016 51 | } 52 | 53 | override fun toString(): String { 54 | return "BleException(code=$code, message='$message')" 55 | } 56 | 57 | class OtherException( 58 | override val code: Int = ERROR_CODE_OTHER, 59 | message: String, 60 | ) : BleException(code, message) { 61 | override fun toString(): String { 62 | return "OtherException(code=$code, message='$message')" 63 | } 64 | } 65 | 66 | class TimeoutException(message: String = "Timeout Exception Occurred!") : 67 | BleException(ERROR_CODE_TIMEOUT, message) { 68 | override fun toString(): String { 69 | return "TimeoutException(code=$code, message='$message')" 70 | } 71 | } 72 | 73 | class DiscoverException( 74 | override val code: Int = ERROR_CODE_GATT, 75 | message: String = "GATT discover services exception occurred!" 76 | ) : BleException(code, message) { 77 | override fun toString(): String { 78 | return "DiscoverException(code=$code, message='$message')" 79 | } 80 | } 81 | 82 | class ConnectException( 83 | val bluetoothGatt: BluetoothGatt?, 84 | val gattStatus: Int, 85 | ) : BleException(ERROR_CODE_GATT, "Gatt Exception Occurred!") { 86 | override fun toString(): String { 87 | return "ConnectException(bluetoothGatt=$bluetoothGatt, gattStatus=$gattStatus) code=$code, message='$message'" 88 | } 89 | } 90 | 91 | class GattException( 92 | val bluetoothGatt: BluetoothGatt?, 93 | val gattStatus: Int, 94 | ) : BleException(ERROR_CODE_GATT, "Gatt Exception Occurred!") { 95 | override fun toString(): String { 96 | return "GattException(bluetoothGatt=$bluetoothGatt, gattStatus=$gattStatus) code=$code, message='$message'" 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/main/java/com/huyuhui/blesample/operate/OperateActivity.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.blesample.operate 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.activity.addCallback 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.core.view.ViewCompat 9 | import androidx.core.view.WindowInsetsCompat 10 | import androidx.core.view.updatePadding 11 | import androidx.navigation.fragment.NavHostFragment 12 | import androidx.navigation.ui.AppBarConfiguration 13 | import androidx.navigation.ui.setupWithNavController 14 | import com.huyuhui.blesample.R 15 | import com.huyuhui.blesample.databinding.ActivityOperateBinding 16 | import com.huyuhui.fastble.BleManager 17 | import com.huyuhui.fastble.data.BleDevice 18 | 19 | class OperateActivity : AppCompatActivity() { 20 | private var bleDevice: BleDevice? = null 21 | private lateinit var binding: ActivityOperateBinding 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | binding = ActivityOperateBinding.inflate(layoutInflater) 25 | setContentView(binding.root) 26 | bleDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 27 | intent.getParcelableExtra("device", BleDevice::class.java) 28 | } else { 29 | @Suppress("DEPRECATION") 30 | intent.getParcelableExtra("device") 31 | } 32 | if (bleDevice == null) { 33 | finish() 34 | } 35 | ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { v: View, insets: WindowInsetsCompat -> 36 | insets.getInsets(WindowInsetsCompat.Type.statusBars()).run { 37 | v.updatePadding(left, top, right, bottom) 38 | } 39 | return@setOnApplyWindowInsetsListener insets 40 | } 41 | ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v: View, insets: WindowInsetsCompat -> 42 | insets.getInsets(WindowInsetsCompat.Type.navigationBars()).run { 43 | v.updatePadding(left, top, right, bottom) 44 | } 45 | return@setOnApplyWindowInsetsListener insets 46 | } 47 | setSupportActionBar(binding.toolbar) 48 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 49 | val navHostFragment = 50 | supportFragmentManager.findFragmentById(R.id.nav_operate_fragment) as NavHostFragment 51 | val navController = navHostFragment.navController 52 | val bundle = Bundle().apply { 53 | putParcelable("device", bleDevice) 54 | } 55 | navController.setGraph(R.navigation.operator_nav, bundle) 56 | 57 | val appBarConfiguration = AppBarConfiguration(topLevelDestinationIds = setOf()) { 58 | finish() 59 | false 60 | } 61 | binding.toolbar.setupWithNavController(navController, appBarConfiguration) 62 | 63 | onBackPressedDispatcher.addCallback(this, true) { 64 | if (!navController.popBackStack()) { 65 | remove() 66 | finish() 67 | } 68 | } 69 | } 70 | 71 | override fun onDestroy() { 72 | super.onDestroy() 73 | BleManager.clearCharacterCallback(bleDevice) 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/com/huyuhui/blesample/operate/ServiceFragment.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.blesample.operate 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.Toast 9 | import androidx.fragment.app.Fragment 10 | import androidx.navigation.fragment.findNavController 11 | import com.huyuhui.fastble.BleManager 12 | import com.huyuhui.fastble.data.BleDevice 13 | import com.huyuhui.blesample.R 14 | import com.huyuhui.blesample.adapter.ServiceAdapter 15 | import com.huyuhui.blesample.databinding.FragmentServiceBinding 16 | 17 | 18 | class ServiceFragment : Fragment() { 19 | private var _binding: FragmentServiceBinding? = null 20 | private val binding 21 | get() = _binding!! 22 | private var bleDevice: BleDevice? = null 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | bleDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 26 | arguments?.getParcelable("device", BleDevice::class.java) 27 | } else { 28 | @Suppress("DEPRECATION") 29 | arguments?.getParcelable("device") 30 | } 31 | } 32 | 33 | override fun onCreateView( 34 | inflater: LayoutInflater, container: ViewGroup?, 35 | savedInstanceState: Bundle? 36 | ): View { 37 | _binding = FragmentServiceBinding.inflate(layoutInflater, container, false) 38 | return binding.root 39 | } 40 | 41 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 42 | super.onViewCreated(view, savedInstanceState) 43 | binding.tvName.text = bleDevice?.name 44 | binding.tvMac.text = bleDevice?.mac 45 | BleManager.getBluetoothGattServices(bleDevice)?.let { 46 | val adapter = ServiceAdapter(requireContext(), it) 47 | binding.lv.setAdapter(adapter) 48 | binding.lv.setOnChildClickListener { _, _, groupPosition, childPosition, _ -> 49 | val bundle = Bundle().apply { 50 | putParcelable("device", bleDevice) 51 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 52 | putParcelable( 53 | "characteristic", 54 | it[groupPosition].characteristics[childPosition] 55 | ) 56 | } else { 57 | putString( 58 | "serviceUUID", 59 | it[groupPosition].uuid.toString() 60 | ) 61 | putString( 62 | "characteristicUUID", 63 | it[groupPosition].characteristics[childPosition].uuid.toString() 64 | ) 65 | } 66 | } 67 | findNavController().navigate(R.id.action_serviceFragment_to_operateFragment, bundle) 68 | true 69 | } 70 | } ?: Toast.makeText(requireContext(), R.string.connection_broken, Toast.LENGTH_SHORT).show() 71 | } 72 | 73 | override fun onDestroyView() { 74 | super.onDestroyView() 75 | _binding = null 76 | } 77 | } -------------------------------------------------------------------------------- /library/src/main/java/com/huyuhui/fastble/queue/Queue.kt: -------------------------------------------------------------------------------- 1 | package com.huyuhui.fastble.queue 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.Job 6 | import kotlinx.coroutines.cancel 7 | import kotlinx.coroutines.channels.Channel 8 | import kotlinx.coroutines.delay 9 | import kotlinx.coroutines.isActive 10 | import kotlinx.coroutines.job 11 | import kotlinx.coroutines.launch 12 | import kotlinx.coroutines.withContext 13 | import kotlinx.coroutines.withTimeoutOrNull 14 | import java.util.concurrent.PriorityBlockingQueue 15 | 16 | internal abstract class Queue : CoroutineScope { 17 | /** 18 | * 某些情况发送失败太快,导致还没有调用receive的情况就开始trySend一直失败,给一个缓冲 19 | */ 20 | private val channel = Channel(1) 21 | private val taskComparator = Comparator { task1, task2 -> 22 | if (task2.priority != task1.priority) { 23 | task2.priority.compareTo(task1.priority) // 逆序排列,优先级高的排在前面 24 | } else { 25 | task1.sequenceNum.compareTo(task2.sequenceNum) 26 | } 27 | } 28 | private val priorityQueue: PriorityBlockingQueue = 29 | PriorityBlockingQueue(10, taskComparator) 30 | private var job: Job? = null 31 | 32 | val remainSize 33 | get() = priorityQueue.size 34 | 35 | fun offer(task: T): Boolean { 36 | return priorityQueue.offer(task) 37 | } 38 | 39 | fun startProcessingTasks() { 40 | resume() 41 | } 42 | 43 | abstract fun execute(task: T, channel: Channel) 44 | fun clear() { 45 | priorityQueue.clear() 46 | } 47 | 48 | fun remove(task: T): Boolean { 49 | return priorityQueue.remove(task) 50 | } 51 | 52 | fun pause() { 53 | job?.cancel() 54 | } 55 | 56 | /** 57 | * receive 可以收到在调用之前通过 trySend 发送的数据,前提是 trySend 成功发送了数据,并且 Channel 没有被关闭 58 | * 可能出现trySend比Receive调用更早的情况 59 | * 如果channel没有缓存,就会出现trySend失败,当continuous为true的时候,就会一直等待withTimeoutOrNull超时,然后取消receive 60 | * 等待priorityQueue.take()下一个任务来的时候,上一次receive已经被取消,又会卡在withTimeoutOrNull,如此循环。如果没有超时逻辑,会一直卡在receive 61 | * 62 | **/ 63 | fun resume() { 64 | if (job?.isActive == true) { 65 | return 66 | } 67 | job = launch(Dispatchers.IO) { 68 | while (isActive) { 69 | val task = priorityQueue.take() 70 | 71 | withContext(Dispatchers.Main) { 72 | //这里会在回调里面trySend 73 | execute(task, channel) 74 | 75 | } 76 | if (task.continuous) { 77 | if (task.timeout > 0) { 78 | withTimeoutOrNull(task.timeout) { 79 | do { 80 | val result = channel.receive() 81 | } while (result.task != task) 82 | return@withTimeoutOrNull 83 | } 84 | } else { 85 | do { 86 | val result = channel.receive() 87 | } while (result.task != task) 88 | } 89 | } 90 | delay(task.delay) 91 | } 92 | } 93 | } 94 | 95 | fun destroy() { 96 | coroutineContext.job.invokeOnCompletion { 97 | channel.close() 98 | } 99 | clear() 100 | cancel() 101 | } 102 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/item_ble_device.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | 28 | 29 | 38 | 39 |