Android 连接 BLE 设备
背景
单位买了一个手持的蓝牙 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.adapterDeviceListView 列表视图,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
评论已关闭