Android Ble Bluetooth App (2) Connection and Discovery Service

foreword

  In the previous article, we processed the scanning device. In this article, we processed the connection and discovery service data. The running effect diagram is as follows:
insert image description here

text

  Now we enter ScanActivity from MainActivity, select a device and return to MainActivity. Next, we need to process the selected device. First, we will make the connection.

1. GATT callback

  We wrote a BleCore before, which encapsulates scanning, so we can also encapsulate it here for connections. We can write a BleGattCallback class in BleCore. The code is as follows:

class BleGattCallback : BluetoothGattCallback() {
    
    

        /**
         * 连接状态改变
         */
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
    
    
            
        }

        /**
         * 发现服务
         */
        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
    
    
            
        }
    }

  Because the things to be done in this article are to connect and discover services, we first rewrite these two functions. Note that the operations of Bluetooth are all carried out in sub-threads. If we need to know whether it is currently connected, we need Write an interface for calling back to the Activity, create a new BleCallback interface under the ble package, the code is as follows:

interface BleCallback {
    
    

    /**
     * 设备的所有信息
     */
    fun deviceInfo(info: String)

    /**
     * 连接状态
     * @param state true or false
     */
    fun onConnectionStateChange(state: Boolean)

    /**
     * 发现服务
     */
    fun onServicesDiscovered(services: List<BluetoothGattService>)
}

  There are three functions defined in the interface. Through the comments, we can clearly know what their functions are. Here we will focus on the first function. This function will display the status information of the device at various times. From all actions after connection, if we need to save the device For the operation log, you can use this to process and save it.

Then go back to BleCore, companion objectdeclare variables and set the interface callback function in:

    @SuppressLint("StaticFieldLeak")
    companion object {
    
    

        ...

        private var mGatt: BluetoothGatt? = null

        private var mBleCallback: BleCallback? = null

        private lateinit var mBleGattCallback: BleGattCallback
        /**
         * 是否连接
         */
        private var mIsConnected = false

        /**
         * 设备信息
         */
        private fun deviceInfo(info: String) = mBleCallback?.deviceInfo(info)
        /**
         * 连接状态
         */
        private fun connectState(state: Boolean) {
    
    
            mIsConnected = state
            mBleCallback?.onConnectionStateChange(state)
        }
    }

At the same time, create a function companion objectoutside of , the code is as follows:

    fun setBleCallback(bleCallback: BleCallback) {
    
    
        mBleCallback = bleCallback
    }

This function setPhyScanCallback()is at the same level as the function, and we will add the connection and disconnection functions below.

2. Connection and disconnection

Add the following code in BleCore:

	/**
     * 连接蓝牙设备
     */
    fun connect(device: BluetoothDevice) {
    
    
        deviceInfo("连接中...")
        mGatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    
    
            device.connectGatt(context, false, mBleGattCallback, BluetoothDevice.TRANSPORT_LE, BluetoothDevice.PHY_LE_2M_MASK)
        } else {
    
    
            device.connectGatt(context, false, mBleGattCallback)
        }
    }

    /**
     * 断开连接
     */
    fun disconnect() {
    
    
        deviceInfo("断开连接...")
        mGatt?.disconnect()
    }

Connection and disconnection will trigger onConnectionStateChange()a function when called.

3. Connection status callback

Let's modify the code of this function as follows:

        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
    
    
            val address = gatt.device.address
            when (newState) {
    
    
                BluetoothProfile.STATE_CONNECTED -> {
    
    
                    deviceInfo("已连接:$address")
                    connectState(true)
                }
                BluetoothProfile.STATE_DISCONNECTED -> {
    
    
                    deviceInfo("已断开连接:$address")
                    connectState(false)
                }
                else -> {
    
    
                    Log.d(TAG, "onConnectionStateChange: $status")
                    connectState(false)
                    mGatt?.close()
                    mGatt = null
                }
            }
        }

In the callback, there will be a corresponding status code for successful connection and disconnection. Call back to the interface function through the status, and then return to MainActivity to use this callback. First, let's modify the code in it, as follows activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/orange"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navigationIcon="@drawable/ic_scan_ble"
        app:title="GoodBle"
        app:titleCentered="true"
        app:titleTextColor="@color/white">

        <TextView
            android:id="@+id/tv_disconnect"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end"
            android:layout_marginEnd="8dp"
            android:visibility="gone"
            android:padding="8dp"
            android:text="断开连接"
            android:textColor="@color/white" />
    </com.google.android.material.appbar.MaterialToolbar>

    <TextView
        android:id="@+id/tv_device_info"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:text="设备信息"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar" />

</androidx.constraintlayout.widget.ConstraintLayout>

Only two TextViews are added in XML, which are used to disconnect and display device status respectively, and then we modify the code in MainActivity as follows:

class MainActivity : BaseActivity(), BleCallback {
    
    

    private val binding by viewBinding(ActivityMainBinding::inflate)

    private lateinit var bleCore: BleCore

    @SuppressLint("MissingPermission")
    private val scanIntent =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
    
     result ->
            if (result.resultCode == Activity.RESULT_OK) {
    
    
                if (result.data == null) return@registerForActivityResult
                //获取选中的设备
                val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    
    
                    result.data!!.getParcelableExtra("device", BluetoothDevice::class.java)
                } else {
    
    
                    result.data!!.getParcelableExtra("device") as BluetoothDevice?
                }
                //连接设备
                if (device != null) bleCore.connect(device)
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        bleCore = (application as BleApp).getBleCore()
        bleCore.setBleCallback(this@MainActivity)
        //进入扫描页面
        binding.toolbar.setNavigationOnClickListener {
    
     scanIntent.launch(Intent(this,ScanActivity::class.java)) }
        //断开连接
        binding.tvDisconnect.setOnClickListener {
    
    
            binding.tvDisconnect.visibility = View.GONE
            bleCore.disconnect()
        }
    }

    override fun deviceInfo(info: String) {
    
    
        runOnUiThread {
    
    
            binding.tvDeviceInfo.text = info
        }
    }

    override fun onConnectionStateChange(state: Boolean) {
    
    
        runOnUiThread {
    
    
            if (state) binding.tvDisconnect.visibility = View.VISIBLE
        }
    }

    override fun onServicesDiscovered(services: List<BluetoothGattService>) {
    
    
        
    }
}

  Here we first StartActivityForResult()jump to the page through the function of the Activity Result API, and get the device object when returning, which has been written in the previous article. After getting the device object, the function called BleCoreto connect()connect the device is onCreate()performed in the function BleCore assignment, and then set the Ble callback, implement BleCallbackthe interface, and rewrite the functions inside. When the connection is successful, deviceInfo()the device status will be obtained through the callback. Because it is a sub-thread, the UI is rendered in the ui thread. As for onConnectionStateChange()the function, the callback connection is successful or failed. If it succeeds, it will be ture, and the control will be displayed. If the tvDisconnectconnection is successful, tvDisconnectclick this to disconnect the connection onCreate().

insert image description here

Judging from this effect diagram, we have a state after the connection is successful, and the state will change when we click to disconnect, then the connection is completed.

4. Discovery service

  After the connection is written, we can write the discovery service below. We can perform the discovery service in the process of successful connection. Next, let's modify the code BleGattCallbackin onConnectionStateChange()the function, as shown in the following figure:

insert image description here

By gatt.discoverServices()performing the action of discovering the service, before setting the current action state through deviceInfo, the execution of the discovery service will trigger a callback. onServicesDiscovered()In this callback, we can call back to the page and modify the code as follows:

        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
    
    
            if (status == BluetoothGatt.GATT_SUCCESS) {
    
    
                deviceInfo("发现了 ${
      
      gatt.services.size} 个服务")
                gatt.services?.let {
    
     mBleCallback?.onServicesDiscovered(it) }
            }
        }

Set the number of discovery services in the callback, and then call back, because there are multiple services, then we need to use a list to load services. First, we modify activity_main.xml and add a RecyclerView in it. The code is as follows :

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout...>

    ...

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_service"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_device_info" />

</androidx.constraintlayout.widget.ConstraintLayout>

5. Service Adapter

  To display the service list data, you first need an adapter, and the adapter needs an item to render the data. Next, we create one under the layout. The code is as follows item_service.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/item_service"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="2dp"
    android:background="@color/white"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_service_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:text="服务"
        android:textColor="@color/black"
        android:textSize="16sp"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_uuid_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="UUID:"
        app:layout_constraintStart_toStartOf="@+id/tv_service_name"
        app:layout_constraintTop_toBottomOf="@+id/tv_service_name" />

    <TextView
        android:id="@+id/tv_service_uuid"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="UUID"
        android:textColor="@color/black"
        app:layout_constraintBottom_toBottomOf="@+id/tv_uuid_title"
        app:layout_constraintStart_toEndOf="@+id/tv_uuid_title"
        app:layout_constraintTop_toTopOf="@+id/tv_uuid_title" />

    <TextView
        android:id="@+id/tv_service_info"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:text="PRIMARY SERVICE"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="@+id/tv_service_name"
        app:layout_constraintTop_toBottomOf="@+id/tv_uuid_title" />

</androidx.constraintlayout.widget.ConstraintLayout>

Next, we create a new class under the ble package BleUtils, the code is as follows:

object BleUtils {
    
    

    private val generic = "-0000-1000-8000-00805F9B34FB"

    /**
     * 获取蓝牙服务名称
     * @param uuid UUID
     */
    fun getServiceName(uuid: UUID) =
        when ("0x${
      
      uuid.toString().substring(4, 8).uppercase(Locale.getDefault())}") {
    
    
            "0x1800" -> "Generic Access service"
            "0x1801" -> "Generic Attribute service"
            "0x1802" -> "Immediate Alert service"
            "0x1803" -> "Link Loss service"
            "0x1804" -> "Tx Power service"
            "0x1805" -> "Current Time service"
            "0x1806" -> "Reference Time Update service"
            "0x1807" -> "Next DST Change service"
            "0x1808" -> "Glucose service"
            "0x1809" -> "Health Thermometer service"
            "0x180A" -> "Device Information service"
            "0x180D" -> "Heart Rate service"
            "0x180E" -> "Phone Alert Status service"
            "0x180F" -> "Battery service"
            "0x1810" -> "Blood Pressure service"
            "0x1811" -> "Alert Notification service"
            "0x1812" -> "Human Interface Device service"
            "0x1813" -> "Scan Parameters service"
            "0x1814" -> "Running Speed and Cadence service"
            "0x1815" -> "Automation IO service"
            "0x1816" -> "Cycling Speed and Cadence service"
            "0x1818" -> "Cycling Power service"
            "0x1819" -> "Location and Navigation service"
            "0x181A" -> "Environmental Sensing service"
            "0x181B" -> "Body Composition service"
            "0x181C" -> "User Data service"
            "0x181D" -> "Weight Scale service"
            "0x181E" -> "Bond Management service"
            "0x181F" -> "Continuous Glucose Monitoring service"
            "0x1820" -> "Internet Protocol Support service"
            "0x1821" -> "Indoor Positioning service"
            "0x1822" -> "Pulse Oximeter service"
            "0x1823" -> "HTTP Proxy service"
            "0x1824" -> "Transport Discovery service"
            "0x1825" -> "Object Transfer service"
            "0x1826" -> "Fitness Machine service"
            "0x1827" -> "Mesh Provisioning service"
            "0x1828" -> "Mesh Proxy service"
            "0x1829" -> "Reconnection Configuration service"
            "0x183A" -> "Insulin Delivery service"
            "0x183B" -> "Binary Sensor service"
            "0x183C" -> "Emergency Configuration service"
            "0x183D" -> "Authorization Control service"
            "0x183E" -> "Physical Activity Monitor service"
            "0x183F" -> "Elapsed Time service"
            "0x1840" -> "Generic Health Sensor service"
            "0x1843" -> "Audio Input Control service"
            "0x1844" -> "Volume Control service"
            "0x1845" -> "Volume Offset Control service"
            "0x1846" -> "Coordinated Set Identification service"
            "0x1847" -> "Device Time service"
            "0x1848" -> "Media Control service"
            "0x1849" -> "Generic Media Control service"
            "0x184A" -> "Constant Tone Extension service"
            "0x184B" -> "Telephone Bearer service"
            "0x184C" -> "Generic Telephone Bearer service"
            "0x184D" -> "Microphone Control service"
            "0x184E" -> "Audio Stream Control service"
            "0x184F" -> "Broadcast Audio Scan service"
            "0x1850" -> " Published Audio Capabilities service"
            "0x1851" -> "Basic Audio Announcement service"
            "0x1852" -> "Broadcast Audio Announcement service"
            "0x1853" -> "Common Audio service"
            "0x1854" -> "Hearing Access service"
            "0x1855" -> "Telephony and Media Audio service"
            "0x1856" -> "Public Broadcast Announcement service"
            "0x1857" -> "Electronic Shelf Label service"
            else -> "Unknown Service"
        }
        
    fun getServiceUUID(uuid: UUID) =
        "0x${
      
      uuid.toString().substring(4, 8).uppercase(Locale.getDefault())}"
}

  Here we need to explain the UUID of Bluetooth, which UUID(Universally Unique Identifier)is an identifier used to uniquely identify Bluetooth devices and services. It is a 128-bit long number that acts as a unique identifier in Bluetooth communication. Bluetooth UUID is divided into two types according to the standard:

  1. 16-bit UUID: These UUIDs are usually used for some common services and characteristics defined by the Bluetooth standard. For example, the UUID for the Device Name service is 00001800-0000-1000-8000-00805F9B34FB.

  2. 128-bit UUID: These UUIDs are often used for custom services and characteristics to ensure global uniqueness. You can generate a 128-bit UUID by yourself as a custom service or feature identifier. For example, a custom service UUID can be 0000XXXX-0000-1000-8000-00805F9B34FB, where the XXXX part can be any hexadecimal number.

In Bluetooth communications, devices use UUIDs to advertise and look up services and identify characteristics. UUID is an important identification when communicating between Bluetooth devices, which ensures the uniqueness of devices and services.

Then getServiceName()you will know what the key in means. 0x1800 is a hexadecimal number, and the corresponding value is SIGdefined. You can refer to this document: Assigned_Numbers.pdf . If you cannot find a corresponding value, it means that it is not standardized by SIG, and your service UUID is customized by your company.

Next, we write the adapter and create a new class under the adapter package ServiceAdapter. The code is as follows:

class ServiceAdapter(
    private val services: List<BluetoothGattService>
) : RecyclerView.Adapter<ServiceAdapter.ViewHolder>() {
    
    

    private var mOnItemClickListener: OnItemClickListener? = null

    fun setOnItemClickListener(mOnItemClickListener: OnItemClickListener?) {
    
    
        this.mOnItemClickListener = mOnItemClickListener
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    
    
        val viewHolder = ViewHolder(ItemServiceBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        viewHolder.binding.itemService.setOnClickListener {
    
     mOnItemClickListener?.onItemClick(it, viewHolder.adapterPosition) }
        return viewHolder
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    
    
        holder.binding.tvServiceName.text = BleUtils.getServiceName(services[position].uuid)
        holder.binding.tvServiceUuid.text = BleUtils.getServiceUUID(services[position].uuid)
    }

    override fun getItemCount() = services.size

    class ViewHolder(itemView: ItemServiceBinding) : RecyclerView.ViewHolder(itemView.root) {
    
    
        var binding: ItemServiceBinding

        init {
    
    
            binding = itemView
        }
    }
}

The code here is relatively simple, which is the basic way of writing. Next, return to MainActivity to display data.

6. Display service

First declare the variable:

    private var mServiceAdapter: ServiceAdapter? = null

    private val mServiceList: MutableList<BluetoothGattService> = mutableListOf()

Then implement OnItemClickListenerthe interface

class MainActivity : BaseActivity(), BleCallback, OnItemClickListener {
    
    

Rewrite the onItemClick() function.

    override fun onItemClick(view: View?, position: Int) {
    
    
        showMsg(mServiceList[position].uuid.toString())
    }

Modify onServicesDiscovered()the function, the code is as follows:

    override fun onServicesDiscovered(services: List<BluetoothGattService>) {
    
    
        runOnUiThread {
    
    
            mServiceList.clear()
            mServiceList.addAll(services)
            mServiceAdapter ?: run {
    
    
                mServiceAdapter = ServiceAdapter(mServiceList)
                binding.rvService.apply {
    
    
                    (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
                    layoutManager = LinearLayoutManager(this@MainActivity)
                    adapter = mServiceAdapter
                }
                mServiceAdapter!!.setOnItemClickListener(this@MainActivity)
                mServiceAdapter
            }
            mServiceAdapter!!.notifyDataSetChanged()
        }
    }

The writing method here is actually exactly the same as the scanning device. Let's run it to see what the effect is.
insert image description here

Seven, source code

If it is helpful to you, you may wish to Star or Fork , the mountain is high and the water is long, there will be a period later~

Source address: GoodBle

Guess you like

Origin blog.csdn.net/qq_38436214/article/details/132085892