背景

单位买了一个手持的蓝牙 RTK 终端,通过蓝牙可以获取到设备的差分定位信息。

蓝牙的 GATT

GATT is an acronym for the Generic ATTribute Profile, and it defines the way that two Bluetooth Low Energy devices transfer data back and forth using concepts called Services and Characteristics. It makes use of a generic data protocol called the Attribute Protocol (ATT), which is used to store Services, Characteristics and related data in a simple lookup table using 16-bit IDs for each entry in the table.

GATT 是Generic ATTributeProfile(通用 ATT 属性配置文件)的首字母缩写,它定义了两个蓝牙低功耗设备使用称为服务和特性的概念来回传输数据的方式。它使用一种名为 "属性协议(ATT)"的通用数据协议,将服务、特性和相关数据存储在一个简单的查找表中,表中每个条目使用 16 位 ID。

GATT comes into play once a dedicated connection is established between two devices, meaning that you have already gone through the advertising process governed by GAP.

一旦两个设备之间建立了专用连接,GATT 就会发挥作用,这意味着您已经通过了 GAP 规定的广告流程。

The most important thing to keep in mind with GATT and connections is that connections are exclusive. What is meant by that is that a BLE peripheral can only be connected to one central device (a mobile phone, etc.) at a time! As soon as a peripheral connects to a central device, it will stop advertising itself and other devices will no longer be able to see it or connect to it until the existing connection is broken.

关于 GATT 和连接,最重要的一点是连接是排他性的。这意味着BLE 外围设备一次只能连接到一个中心设备(手机等)!一旦外设连接到中央设备,它将停止自我宣传,其他设备将无法再看到它或连接到它,直到现有连接中断。

Establishing a connection is also the only way to allow two way communication, where the central device can send meaningful data to the peripheral and vice versa.

建立连接也是实现双向通信的唯一途径,中央设备可以向外围设备发送有意义的数据,反之亦然。

也就是说,我们需要知道这个 BLE 设备的 GATT 配置

连接 BLE

AndroidManifest 里面填写需要的权限,使用蓝牙搜索时需要打开定位,所有别忘了位置权限。申请运行时权限 Android Runtime Permissions,到达 All permissions granted (所有权限被授予)。

定义变量

class  MainActivity : AppCompatActivity() {

    private lateinit var bluetoothAdapter: BluetoothAdapter
    private lateinit var deviceListView: ListView
    private lateinit var scanButton: Button

    private val deviceList = mutableListOf<BluetoothDevice>()
    private lateinit var deviceListAdapter: ArrayAdapter<String>

....

这里定义了5个变量:
BluetoothAdapter 蓝牙适配器,这是一个 Android 类,用于访问设备的蓝牙硬件,它代表本地蓝牙适配器,用于与蓝牙设备交互。通过BluetoothAdapter,可以执行启动或停止蓝牙扫描、检查蓝牙状态(启用/禁用)和管理蓝牙连接等操作。在与蓝牙设备交互(如扫描附近的设备)之前,需要获取系统蓝牙适配器的引用。

val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter

DeviceListView 列表视图,ListView 是一种 UI 组件,用于显示可垂直滚动的项目列表。在这种情况下,它用于显示扫描过程中发现的蓝牙设备列表。扫描完成后,ListView 会显示附近蓝牙设备的名称和地址。用户可从该列表中选择设备建立连接。相应的ArrayAdapter用于将设备列表绑定到ListView

deviceListView = findViewById(R.id.device_list)

onCreate() 代码

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

    // Apply for permission
    if (!hasAllPermissions()) {
        requestAllPermissions()
    } else {
        proceedWithAppFunctionality()
    }

    // Initialize UI elements
    deviceListView = findViewById(R.id.device_list)
    scanButton = findViewById(R.id.scan_button)

    // Set up the scan button click listener
    scanButton.setOnClickListener {
        startBleScan()
    }

    // Handle list item clicks to navigate to SecondActivity
    deviceListView.setOnItemClickListener { _, _, position, _ ->
        val selectedDevice = deviceList[position]
        val intent = Intent(this, SecondActivity::class.java).apply {
            putExtra("device_address", selectedDevice.address)
        }
        startActivity(intent)
    }

    // Set up ListView adapter
    deviceListAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, mutableListOf<String>())
    deviceListView.adapter = deviceListAdapter

    // Initialize Bluetooth adapter
    val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    bluetoothAdapter = bluetoothManager.adapter

    // Check if Bluetooth is supported and enabled
    if (!packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
        Toast.makeText(this, "BLE not supported", Toast.LENGTH_SHORT).show()
        finish() // Exit the app if BLE is not supported
    } else if (!bluetoothAdapter.isEnabled) {
        // Request to enable Bluetooth if it's disabled
        try {
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            startActivityForResult(enableBtIntent, 1)
        } catch (e: SecurityException) {
            handleSecurityException(e)
        }
    }
}

蓝牙扫描函数startBleScan()

private fun startBleScan() {
    deviceList.clear()
    deviceListAdapter.clear()

    try {
        val bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
        val leScanCallback = object : ScanCallback() {
            override fun onScanResult(callbackType: Int, result: ScanResult) {
                super.onScanResult(callbackType, result)
                val device = result.device
                if (device.name != null && !deviceList.contains(device)) {
                    deviceList.add(device)
                    deviceListAdapter.add("${device.name} - ${device.address}")
                    deviceListAdapter.notifyDataSetChanged()
                }
            }

            override fun onScanFailed(errorCode: Int) {
                Log.e("BLE", "Scan failed with error code: $errorCode")
            }
        }

        bluetoothLeScanner.startScan(leScanCallback)

        // Stop the scan after 10 seconds
        Handler(Looper.getMainLooper()).postDelayed({
            bluetoothLeScanner.stopScan(leScanCallback)
        }, 10000)
    } catch (e: SecurityException) {
        handleSecurityException(e)
    }
}


private fun handleSecurityException(e: SecurityException) {
    // Handle the exception gracefully
    Log.e("BLE", "SecurityException: ${e.message}")
    Toast.makeText(this, "Permissions are required for this operation.", Toast.LENGTH_SHORT).show()
    // You could also prompt the user to grant the required permissions
}

activity_main 代码

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

    <!-- A TextView to display the scanning status -->
    <TextView
            android:id="@+id/status_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Scanning for devices..."
            android:textSize="18sp"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="16dp" />

    <!-- A ListView to show the list of discovered devices -->
    <ListView
            android:id="@+id/device_list"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/status_text"
            android:layout_marginTop="16dp" />

    <!-- A Button to manually start scanning -->
    <Button
            android:id="@+id/scan_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Start Scan"
            android:layout_below="@id/device_list"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="16dp" />

</RelativeLayout>

接收 BLE 数据

MainActivity中扫描到的蓝牙设备,点击后连接设备,然后进入SecondActivity接收数据

class SecondActivity : AppCompatActivity() {

    private lateinit var statusTextView: TextView
    private lateinit var receivedDataTextView: TextView
    private lateinit var actionButton: Button

    private lateinit var bluetoothAdapter: BluetoothAdapter
    private var bluetoothGatt: BluetoothGatt? = null

    private val serviceUUID = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb")
    private val characteristicUUID = UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb")
    private val descriptorUUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")

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

        // Initialize UI elements
        statusTextView = findViewById(R.id.status_text)
        receivedDataTextView = findViewById(R.id.received_data_text)
        actionButton = findViewById(R.id.action_button)

        // Retrieve the device address from the intent
        val deviceAddress = intent.getStringExtra("device_address") ?: return

        // Initialize Bluetooth adapter
        val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        bluetoothAdapter = bluetoothManager.adapter

        // Get the BluetoothDevice object and connect to it
        val device = bluetoothAdapter.getRemoteDevice(deviceAddress)
        connectToDevice(device)

        // Set up button click listener
        actionButton.setOnClickListener {
            // Disconnect the Bluetooth GATT connection
            disconnectDevice()
        }
    }

    private fun connectToDevice(device: BluetoothDevice) {
        statusTextView.text = "Connecting to ${device.name ?: "Unnamed Device"}"

        bluetoothGatt = device.connectGatt(this, false, object : BluetoothGattCallback() {
            override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    runOnUiThread {
                        statusTextView.text = "Connected to ${device.name ?: "Unnamed Device"}"
                    }
                    gatt.discoverServices()
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    runOnUiThread {
                        statusTextView.text = "Disconnected from ${device.name ?: "Unnamed Device"}"
                    }
                }
            }

            override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    runOnUiThread {
                        statusTextView.text = "Services discovered"
                    }
                    val characteristic = gatt.getService(serviceUUID)?.getCharacteristic(characteristicUUID)

                    characteristic?.let {
                        val properties = characteristic.properties
                        if (properties and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0) {
                            enableIndications(gatt, characteristic)
                        } else if (properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0) {
                            enableNotifications(gatt, characteristic)
                        } else {
                            runOnUiThread {
                                statusTextView.text = "Characteristic does not support indications or notifications"
                            }
                        }
                    } ?: runOnUiThread {
                        statusTextView.text = "Characteristic not found"
                    }
                } else {
                    Log.e("BLEE", "Service discovery failed with status: $status")
                }
            }

            override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
                val data = characteristic.value
                runOnUiThread {
                    receivedDataTextView.text = "Data: ${data.toString(Charsets.UTF_8)}"
                }
            }
        })
    }

    private fun enableIndications(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
        gatt.setCharacteristicNotification(characteristic, true)
        val descriptor = characteristic.getDescriptor(descriptorUUID)
        descriptor?.let {
            it.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
            gatt.writeDescriptor(it)
        }
    }

    private fun enableNotifications(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
        gatt.setCharacteristicNotification(characteristic, true)
        val descriptor = characteristic.getDescriptor(descriptorUUID)
        descriptor?.let {
            it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
            gatt.writeDescriptor(it)
        }
    }

    private fun disconnectDevice() {
        bluetoothGatt?.disconnect()
        bluetoothGatt?.close()
        bluetoothGatt = null
        statusTextView.text = "Disconnected"
    }

    override fun onDestroy() {
        super.onDestroy()
        disconnectDevice()
    }
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SecondActivity">

    <!-- TextView to display connection status -->
    <TextView
        android:id="@+id/status_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Connecting..."
        android:textSize="18sp"
        android:textColor="@android:color/holo_blue_dark"
        android:layout_marginTop="20dp"
        android:layout_marginBottom="16dp"
        android:layout_marginHorizontal="16dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <!-- ScrollView for displaying continuously received data -->
    <ScrollView
        android:id="@+id/scroll_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginHorizontal="16dp"
        app:layout_constraintTop_toBottomOf="@id/status_text"
        app:layout_constraintBottom_toTopOf="@id/action_button"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">

        <TextView
            android:id="@+id/received_data_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="No data received yet"
            android:textSize="16sp"
            android:padding="8dp"
            android:background="#E0E0E0"
            android:textColor="#000000"
            android:minHeight="200dp"
            android:scrollbars="vertical" />
    </ScrollView>

    <!-- Button to disconnect or perform an action -->
    <Button
        android:id="@+id/action_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Disconnect"
        android:layout_marginBottom="20dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

代码来自 GPT-4o

标签: none

评论已关闭