前言:
连接采用nordicsemi库,其中nRF Connect也是使用该库。
蓝牙连接库、nordicsemi官网、nRF Connect apk使用教程、nRF Connect apk下载地址、蓝牙UUID介绍
效果图:
首先app\build.gradle加入:
//蓝牙库
implementation 'no.nordicsemi.android:ble:2.2.4'
快捷找ID路径如上:
id 'kotlin-android-extensions'
1、蓝牙管理类MyBleManager:
/**
* 蓝牙UUID介绍:
* 基本的UUID:0000xxxx-0000-1000-8000-00805F9B34FB【设备可自定义各种格式】
* 类说明:蓝牙管理类
*/
class MyBleManager(context: Context) : BleManager(context) {
private val TAG = "MyBleManager"
//服务UUID
private val SERVICE_UUID: UUID = UUID.fromString("0000001-0000-1000-8000-00805F9B34FB")
//写入UUID
private val WRITE_UUID = UUID.fromString("00000002-0000-1000-8000-00805F9B34FB")
//特征码UUID【监听】
private val NOTIFY_UUID = UUID.fromString("00000003-0000-1000-8000-00805F9B34FB")
//蓝牙GATT特性
private var writeChar: BluetoothGattCharacteristic? = null
private var notifyChar: BluetoothGattCharacteristic? = null
//设备是否需要Dfu升级【后续预留DFU设备升级】
var isDeviceUpdater = false
override fun getGattCallback(): BleManagerGattCallback {
return MyBleManagerGattCallback()
}
/**
* Gatt回调类,蓝牙连接过程中的操作一般都是通过此类来完成
*/
private inner class MyBleManagerGattCallback : BleManagerGattCallback() {
public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
//校验设备是否拥有我们所需的服务与特征
val service = gatt.getService(SERVICE_UUID)
//是否需要更新
if (isDeviceUpdater && null == service) {
return true
}
if (service != null) {
//读写特征
writeChar = service.getCharacteristic(WRITE_UUID)
//通知特征
notifyChar = service.getCharacteristic(NOTIFY_UUID)
}
//校验读写特征是否拥有写入数据的权限
var notify = false
if (notifyChar != null) {
val properties = notifyChar!!.properties
notify = properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0
}
var writeRequest = false
if (writeChar != null) {
val properties = writeChar!!.properties
writeRequest =
properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE != 0
writeChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
}
//如果找到了所有需要的服务,则返回true
return writeChar != null && notifyChar != null && notify && writeRequest
}
//在这里初始化设备。通常需要启用通知和设置required
override fun initialize() {
//创建一个原子请求队列。来自队列的请求将按顺序执行。
beginAtomicRequestQueue()
//最大20字节,GATT需要额外3字节。这将允许数据包大小为244字节
.add(requestMtu(23)
.with { device: BluetoothDevice?, mtu: Int ->
Log.e(TAG, "with: $mtu")
}
.fail { device: BluetoothDevice?, status: Int ->
Log.e(TAG, "fail: $status")
})
.add(enableNotifications(notifyChar))
.done { device: BluetoothDevice? ->
Log.e(TAG, "done: " + device!!.name)
Log.e(TAG, "done: " + device.address)
}
.enqueue()
//通过notificationDataCallback进行通知监听
setNotificationCallback(notifyChar)
.with { device: BluetoothDevice?, data: Data? ->
listener.invoke(device, data!!.value)
}
//enableNotifications(notifyChar).enqueue();
}
/**
* 断开连接
*/
override fun onDeviceDisconnected() {
//设备断开连接。置null
writeChar = null
notifyChar = null
}
}
/**
* 通知数据回调
*/
private lateinit var listener: (BluetoothDevice?, ByteArray?) -> Unit
fun setOnListener(deviceData: (device: BluetoothDevice?, byteArray: ByteArray?) -> Unit) {
listener = deviceData
}
}
2、蓝牙连接类ConnectUtils:
/**
* 类说明:蓝牙连接类
*/
class ConnectUtils private constructor() : ConnectionObserver {
private val TAG = "ConnectUtils"
//设备管理类
private var bleManager: MyBleManager? = null
//dfu升级【预留】
var isDfuUpdate = false
/**
* 单例
* Lazy是接受一个 lambda 并返回一个 Lazy 实例的函数,返回的实例可以作为实现延迟属性的委托:
* 第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果
*/
companion object {
val getInstance: ConnectUtils by lazy {
ConnectUtils()
}
}
/**
* 开始连接设备
*/
fun startConnect(context: Context, device: BluetoothDevice) {
Log.e(TAG, "startConnect: 连接的设备名:" + device.name)
bleManager = MyBleManager(context)
//设置连接观察器
bleManager!!.setConnectionObserver(this)
//数据回调
bleManager!!.setOnListener { device, byteArray ->
//从结构上来看apply函数和run函数很像,唯一不同点就是它们各自返回的值不一样,
//run函数是以闭包形式返回最后一行代码的值,而apply函数的返回的是传入对象的本身。
byteArray.apply {
//返回基础字节数组。
Log.e(TAG, "响应的数据ByteArray: " + ByteUtils.bytesToString(byteArray!!))
}
}
//开始连接
bleManager!!.connect(device)
//是否断连自动重连
.useAutoConnect(true)
//连接超时10秒
.timeout(10000)
//重连的次数,每次重连延时100
.retry(3, 100)
//设置完成回调
.done {
//连接完成,关闭加载框
LoadingDialog.closeTimerDialog()
Log.e(TAG, "startConnect: 初始化连接成功")
}
//将请求排队以进行异步执行。
.enqueue()
}
/**
* 断开连接
*/
fun disconnect() {
if (null != bleManager) {
bleManager!!.disconnect()
bleManager!!.close()
}
}
override fun onDeviceDisconnecting(device: BluetoothDevice) {
Log.e(TAG, "onDeviceDisconnecting: 设备断开连接中...")
listener.invoke(device, "设备断开连接中")
}
override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) {
Log.e(TAG, "onDeviceDisconnected: 设备断开连接" + device.name)
Log.e(TAG, "onDeviceDisconnected: 设备断开连接" + device.address)
listener.invoke(device, "设备断开连接")
}
override fun onDeviceReady(device: BluetoothDevice) {
//方法在所有初始化请求都已完成时调用
Log.e(TAG, "onDeviceReady: " + device.name)
Log.e(TAG, "onDeviceReady: " + device.address)
}
override fun onDeviceConnected(device: BluetoothDevice) {
Log.e(TAG, "onDeviceConnected: 设备已连接 ")
listener.invoke(device, "设备已连接")
}
override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) {
Log.e(TAG, "onDeviceFailedToConnect: 设备连接失败超时$reason")
listener.invoke(device, "设备连接超时")
}
override fun onDeviceConnecting(device: BluetoothDevice) {
Log.e(TAG, "onDeviceConnecting: 设备连接中...")
listener.invoke(device, "设备连接中")
}
/**
* 连接状态回调
*/
private lateinit var listener: (BluetoothDevice, String) -> Unit
fun setOnListener(deviceData: (device: BluetoothDevice, state: String) -> Unit) {
listener = deviceData
}
}
3、使用类MainActivity:
/**
* 类说明:扫描+连接
*/
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private var bluetoothDevice: BluetoothDevice? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
BleScanUtils.getInstance.startScanBle(this)
BleScanUtils.getInstance.setOnListener {
for (i in it) {
if (i.name == "KFC 6666") {
bluetoothDevice = i
//扫描到关闭扫描
BleScanUtils.getInstance.stopScanBle()
}
Log.e(TAG, "onCreate: " + i.address)
Log.e(TAG, "onCreate: " + i.name)
}
}
startConnectBtn.setOnClickListener {
if (null != bluetoothDevice) {
if (startConnectBtn.text.toString() == "断开连接") {
//断开连接【手动断连不走回调】
ConnectUtils.getInstance.disconnect()
startConnectBtn.text = "开始连接"
connectStateTv.text = "设备已断连"
} else {
//开始连接
LoadingDialog.showTimerDialog(this)
ConnectUtils.getInstance.startConnect(this@MainActivity, bluetoothDevice!!)
}
}
}
//连接状态
ConnectUtils.getInstance.setOnListener { device, state ->
connectStateTv.text = state
startConnectBtn.text = "断开连接"
}
}
/**
* 关闭扫描
*/
override fun onDestroy() {
super.onDestroy()
BleScanUtils.getInstance.stopScanBle()
}
}
对应布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/connectStateTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:text="设备未连接"
android:textSize="18sp" />
<Button
android:id="@+id/startConnectBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:text="开始连接" />
</LinearLayout>
4、蓝牙工具类ByteUtils:
/**
* 类说明:蓝牙工具类
*/
class ByteUtils {
companion object {
private val STRING_ARRAY = "0123456789ABCDEF".toCharArray()
/**
* 字节数组转16进制Hex字符串【大写】
*/
fun bytesToString(bytes: ByteArray): String {
val hexChars = CharArray(bytes.size * 2)
for (j in bytes.indices) {
val v: Int = bytes[j].toInt() and 0xFF
hexChars[j * 2] = STRING_ARRAY[v ushr 4]
hexChars[j * 2 + 1] = STRING_ARRAY[v and 0x0F]
}
return String(hexChars)
}
/**
* 字节数组转16进制Hex字符串【小写】
*/
// fun bytesToHex(bytes: ByteArray): String? {
// val sb = StringBuffer()
// for (i in bytes.indices) {
// val hex = Integer.toHexString(bytes[i].toInt() and 0xFF)
// if (hex.length < 2) {
// sb.append(0)
// }
// sb.append(hex)
// }
// return sb.toString()
// }
}
}
5、连接加载框:LoadingDialog
/**
* 类说明:连接加载框
*/
class LoadingDialog {
companion object {
private lateinit var dialog: Dialog
private lateinit var timer: CountDownTimer
/**
* 倒计时弹框
*/
fun showTimerDialog(activity: AppCompatActivity) {
showProgressDialog(activity)
//计时10秒;每隔一秒执行
timer = object : CountDownTimer(10000, 1000) {
override fun onTick(millisUntilFinished: Long) {
}
override fun onFinish() {
Toast.makeText(activity, "蓝牙连接连接超时", Toast.LENGTH_SHORT).show()
closeTimerDialog()
}
}.start()
}
/**
* 关闭计时器
*/
fun closeTimerDialog() {
closeDialog()
timer.cancel()
}
/**
* 显示弹框
*/
private fun showProgressDialog(context: Context) {
dialog = Dialog(context, R.style.dialog_style)
dialog.setContentView(R.layout.loading_dialog)
dialog.setCanceledOnTouchOutside(false)//点击外部不关闭
dialog.setCancelable(false)//点击返回键不关闭
dialog.show()//显示弹框
}
/**
* 关闭弹框
*/
private fun closeDialog() {
if (null != dialog) {
dialog.dismiss()
}
}
}
}
对应布局loading_dialog.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
android:background="@drawable/linear_shape"
android:gravity="center"
android:padding="20dp">
<!--indeterminateDuration:转一圈的时间0.8秒-->
<ProgressBar
android:id="@+id/loadProgressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminateDrawable="@drawable/progress_shape"
android:indeterminateDuration="800" />
</LinearLayout>
</RelativeLayout>
Dialog样式:
<style name="dialog_style" parent="@android:style/Theme.Dialog">
<!-- 设置未浮动窗口 -->
<item name="android:windowIsFloating">true</item>
<!-- 设置无边框 -->
<item name="android:windowFrame">@null</item>
<!-- 设置无标题 -->
<item name="android:windowNoTitle">true</item>
<!-- 设置完全透明-->
<item name="android:windowBackground">@android:color/transparent</item>
<!-- 设置屏幕变暗 -->
<item name="android:backgroundDimEnabled">true</item>
</style>
资源文件:
linear_shape.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="10dp" />
<solid android:color="#ffffff" />
</shape>
progress_shape.xml
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="360">
<shape
android:innerRadiusRatio="3"
android:shape="ring"
android:thicknessRatio="10"
android:useLevel="false">
<gradient
android:centerColor="#333333"
android:centerY="0.5"
android:endColor="#666666"
android:startColor="#999999"
android:type="sweep"
android:useLevel="false" />
</shape>
</rotate>
连接过程:
主动关机设备,在开机重连过程:
下一篇:数据传输的两种方式和设备Dfu升级。