前言:
连接采用nordicsemi库,其中nRF Connect也是使用该库。

蓝牙连接库nordicsemi官网nRF Connect apk使用教程nRF Connect apk下载地址蓝牙UUID介绍

效果图:

android ble keyboard android ble keyboard connect_安卓

首先app\build.gradle加入:

//蓝牙库
  implementation 'no.nordicsemi.android:ble:2.2.4'

快捷找ID路径如上:

android ble keyboard android ble keyboard connect_android_02

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>

连接过程:

android ble keyboard android ble keyboard connect_ide_03


主动关机设备,在开机重连过程:

android ble keyboard android ble keyboard connect_安卓_04


下一篇:数据传输的两种方式和设备Dfu升级。