基于RFCOMM协议实现的经典蓝牙基础性能测试应用

116 阅读12分钟

一、前置知识


本文主要介绍的是基于RFCOMM协议实现的经典蓝牙基础性能测试应用Demo的实现及方案选型。为便于理解,本文分为两个板块,第一个板块会从相关的蓝牙基础知识开始介绍,第二个板块主要介绍Demo使用的蓝牙基本性能测试项的选项和实现思路。

1.1 Android平台对蓝牙的支持

1.1.1 官方资料

官方文档对Android平台下蓝牙模块的可用API和使用方式都有详尽的介绍,如有兴趣可以自行了解:developer.android.google.cn/develop/con…

1.1.2 概括

蓝牙的使用无非要历经扫描附近可用的设备,发起配对,建立连接,传输数据等阶段,在这个过程中需要注意的是Android平台目前针对“经典蓝牙”和“蓝牙低功耗(BLE)”两类蓝牙设备提供了两套标准API,因此使用时要注意根据实际的业务场景来决定要使用哪一套。特别注意这两套API不可混合使用,避免重复扫描或是其他不可控因素的出现。

接下来简要介绍这两类设备的差异及对应的API。

1.2 经典蓝牙和BLE蓝牙

1.2.1 一图流说明

image.png

1.2.2 拓展

经典蓝牙(Classic Bluetooth)和低功耗蓝牙(BLE)是两种不同的蓝牙技术,它们在应用、功耗和支持的协议版本等方面有很大的区别:

(1)应用和用途: 经典蓝牙:用于在较短距离内(通常不超过10米)进行高速数据传输,如音频传输(例如耳机、音响系统)、 文件传输、键盘和鼠标连接等。 低功耗蓝牙(BLE):主要用于低功耗应用,如传感器、健康和医疗设备、物联网(IoT)设备等,它通常在较长时间内维持连接并以最低功耗传输小量数据。

(2)数据传输速度: 经典蓝牙:支持更高的数据传输速度,适用于需要传输大量数据的应用。 低功耗蓝牙(BLE):传输速度较低,适用于小型数据包的周期性传输。

(3)功耗: 经典蓝牙:通常功耗较高,不适合长时间的低功耗操作,因此不适用于电池寿命要求较高的设备。 低功耗蓝牙(BLE):设计用于低功耗操作,能够在较长时间内维持连接,适用于电池供电设备。

(4)连接方式: 经典蓝牙:使用较复杂的连接过程,需要建立连接并保持连接。 低功耗蓝牙(BLE):支持快速连接和断开,以节省能量。

(5)协议版本: 经典蓝牙:支持不同版本的规范,如Bluetooth 2.0、Bluetooth 3.0、Bluetooth 4.0、Bluetooth 5.0等。 低功耗蓝牙(BLE):支持不同版本的规范,如Bluetooth 4.0、Bluetooth 4.1、Bluetooth 4.2、Bluetooth 5.0、Bluetooth 5.1和Bluetooth 5.2。每个版本都引入了新的功能和改进,如安全性增强、速度提升、广播范围增加等。

1.3 Android平台下对两类蓝牙设备的API支持

1.3.1 两类蓝牙设备API在使用前都需要的步骤

(1)申请权限,参考官方文档即可:developer.android.google.cn/develop/con…

(2)获取bluetoothAdapter,确认设备是否支持蓝牙

val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java)
val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.getAdapter()
if (bluetoothAdapter == null) {
  // Device doesn't support Bluetooth
}

(3)启用蓝牙 同样通过bluetoothAdapter,调用isEnabled确认设备是否支持蓝牙

if (bluetoothAdapter?.isEnabled == false) {
  val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
  startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}

在用户授权并打开蓝牙后,我们就可以同时使用传统蓝牙和蓝牙低功耗(BLE)。

1.3.2 经典蓝牙工作流程简介

(1)查询已配对设备

val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices
pairedDevices?.forEach { device ->
   val deviceName = device.name
   val deviceHardwareAddress = device.address // MAC address
}

(2)发现设备 主要是通过bluetoothAdapter.startDiscovery(),然后接收BluetoothDevice.ACTION_FOUND广播即可。

override fun onCreate(savedInstanceState: Bundle?) {
   ...

   // Register for broadcasts when a device is discovered.
   val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)
   registerReceiver(receiver, filter)
}

// Create a BroadcastReceiver for ACTION_FOUND.
private val receiver = object : BroadcastReceiver() {

   override fun onReceive(context: Context, intent: Intent) {
       val action: String = intent.action
       when(action) {
           BluetoothDevice.ACTION_FOUND -> {
               // Discovery has found a device. Get the BluetoothDevice
               // object and its info from the Intent.
               val device: BluetoothDevice =
                       intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
               val deviceName = device.name
               val deviceHardwareAddress = device.address // MAC address
           }
       }
   }
}

override fun onDestroy() {
   super.onDestroy()
   ...

   // Don't forget to unregister the ACTION_FOUND receiver.
   unregisterReceiver(receiver)
}

(3)设置设备可见性

Tips:标准的Settings中,只有当用户处于Settings的蓝牙界面时,才会把设备的蓝牙可见性设置为可见,否则一般默认是仅可连接,也就是只有曾经配对过的设备才能看见当前设备,如果需要在某些特定业务场景下允许当前设备能被其他设备发现,不妨自己主动设置当前设备为可见。注意,设置当前设备可见性会增加功耗,对于功耗敏感型的设备需谨慎使用;另外,对于通常应用来讲,只能设置设备为可见,但无法主动设置当前设备为隐藏或是仅可连接,因为设置可见性的接口对应用层是隐藏的,通常只有系统级应用才允许使用。

val requestCode = 1;
//设置当前设备五分钟内可见
val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
   putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300)
}
startActivityForResult(discoverableIntent, requestCode)

1.3.3 低功耗蓝牙(BLE)工作流程简介

(1)低功耗蓝牙(BLE)核心概念简介

1.GATT (Generic Attribute Profile): 定义了 BLE 设备间数据交换的规范和格式。

2.服务 (Service): 相关特性的集合,用于实现设备上的一个功能。每个服务由一个唯一的 UUID 标识。

3.特性 (Characteristic): 一个数据值,包含了设备的状态或数据。它可以是可读的、可写的、或支持通知/指示的。每个特性也由一个 UUID 标识。

4.描述符 (Descriptor): 提供关于特性的更多信息,例如可读的描述或如何格式化特性值。

举例来说: 你有一台 Android 手机和一台支持 BLE 的设备(如家居传感器),该外围BLE设备会将传感器数据报告回手机。 手机(中央设备)会主动扫描 BLE 设备。外围设备会进行广播,并等待收到连接请求。 手机与外围BLE设备建立连接后,它们便开始相互传送 GATT 元数据。在这个例子中,手机上运行的应用会发送数据请求,因此它充当 GATT 客户端。外围的BLE设备则会执行这些请求,因此它充当 GATT 服务器。

实现流程主要有以下几个阶段:

(2)查找BLE设备 主要区别,扫描BLE设备使用的是bluetoothAdapter.bluetoothLeScanner.startScan(ScanCallback callback)

private val bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
private var scanning = false
private val handler = Handler()

// Stops scanning after 10 seconds.
private val SCAN_PERIOD: Long = 10000

private fun scanLeDevice() {
    if (!scanning) { // Stops scanning after a pre-defined scan period.
        handler.postDelayed({
            scanning = false
            bluetoothLeScanner.stopScan(leScanCallback)
        }, SCAN_PERIOD)
        scanning = true
        bluetoothLeScanner.startScan(leScanCallback)
    } else {
        scanning = false
        bluetoothLeScanner.stopScan(leScanCallback)
    }
}

在设置的Callback内接受扫描结果:

private val leDeviceListAdapter = LeDeviceListAdapter()
// Device scan callback.
private val leScanCallback: ScanCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult) {
        super.onScanResult(callbackType, result)
        leDeviceListAdapter.addDevice(result.device)
        leDeviceListAdapter.notifyDataSetChanged()
    }
}

(3)从扫描结果中选择一个设备进行连接

1.定义GATT回调BluetoothGattCallback

// 用于管理已连接的 GATT 客户端
var connectedGatt by remember { mutableStateOf<BluetoothGatt?>(null) }
var servicesDiscovered by remember { mutableStateOf(false) }

val gattCallback = remember {
    object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                Log.i("GATT", "Connected to GATT server.")
                connectedGatt = gatt
                // 连接成功后,立即开始发现服务
                scope.launch(Dispatchers.IO) {
                    gatt.discoverServices()
                }
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                Log.i("GATT", "Disconnected from GATT server.")
                connectedGatt?.close()
                connectedGatt = null
                servicesDiscovered = false
            }
        }

        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.i("GATT", "Services discovered.")
                servicesDiscovered = true
                // 遍历 gatt.services 来查看所有可用的服务和特性
                gatt.services.forEach { service ->
                    Log.d("GATT_SERVICE", "Service UUID: ${service.uuid}")
                    service.characteristics.forEach { characteristic ->
                        Log.d("GATT_CHAR", "  - Characteristic UUID: ${characteristic.uuid}")
                    }
                }
            } else {
                Log.w("GATT", "onServicesDiscovered received: $status")
            }
        }

        override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, status: Int) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                val data = value.toString(Charsets.UTF_8) // 假设是 UTF-8 编码
                Log.i("GATT_READ", "Read characteristic ${characteristic.uuid}: $data")
                // 更新 UI ...
            }
        }

        override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.i("GATT_WRITE", "Wrote to characteristic ${characteristic.uuid} successfully")
            }
        }
        
        // onCharacteristicChanged 订阅通知后,数据变化时被调用
        override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) {
            val data = value.toString(Charsets.UTF_8)
            Log.i("GATT_NOTIFY", "Characteristic ${characteristic.uuid} changed: $data")
            // 更新 UI ...
        }
    }

2.对发现的蓝牙低功耗(BLE)设备发起连接

Button(
    onClick = {
        if (connectedGatt?.device == device) {
             // 断开连接
            connectedGatt?.disconnect()
        } else {
			...
            // 连接到设备
			//gattCallback就是上一步中定义好的gatt回调
            device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
        }
    },
	...
) {
	...
}

二、基于传统蓝牙实现的蓝牙基本功能测试Demo

2.1 需求描述

从应用层角度检查当前设备蓝牙基本功能(配对/连接)是否正常,并给出可用于反映蓝牙基本性能的测试项。

2.2 主要功能实现思路

本Demo基于Kotlin+Compose实现,由于使用场景不涉及低功耗蓝牙(BLE)设备的通信,因此实现仅考虑传统蓝牙场景。

2.2.1 配对/连接测试

将设备分为待测机和配合机,配合机只需保证能被其他设备发现即可。 以下为Demo主页面总览,左侧为功能区,中间栏为已配对设备显示区,右侧栏则为附近可用蓝牙设备列表展示和测试结果显示区。

Screenshot_20251203_143314.png 要测试蓝牙配对/连接的功能,待测机点击扫描设备,在右侧列表中发现配合机点击发起连接即可,配对成功配合机会显示在中间已配对设备列表中,这部分功能基于前文提到的经典蓝牙扫描和连接的标准API实现即可。

2.2.2 蓝牙基本性能测试

这些高级测试项都涉及蓝牙数据传输,因此需要待测机和配合机都安装此应用进行测试。蓝牙通信本身支持多种协议,最常见的有以下几种:

•A2DP (高级音频分发协议):专门用来传输高质量立体声音频的工具(例如,手机连接蓝牙音箱播放音乐)。

•HID (人机接口设备协议):专门用来传输控制指令的工具(例如,连接蓝牙键盘、鼠标)。

•HFP (免提协议):专门用于语音通话的工具(例如,连接车载蓝牙讲电话)。

•SPP (串行端口协议):提供了一个通用的、像串口一样的数据传输通道。它是最基础的数据管道之一。

我们目前的需求只是对待测设备的蓝牙基本功能进行测试,不涉及大批量或是音频数据的传输,因此选择最基础的通信协议SPP即可,并且由于此协议较为简单,不涉及复杂的音视频编解码逻辑,从一定程度上更能反映设备蓝牙的基本性能。这里补充一个概念RFCOMM(Radio Frequency Communication)串行通信协议,它能使蓝牙设备能够模拟传统的串口通信(类似于 RS-232),提供一个可靠的方式进行数据传输,而前面我们提到的SPP(Serial Port Profile)则是一种蓝牙配置文件,基于 RFCOMM 协议。这一配置文件定义了如何在蓝牙设备之间实现串行端口的通信,因此严格来说,在本demo中待测机和配合机直接通过RFCOMM协议进行蓝牙通信。

有了以上的概念,为了后续的蓝牙基本性能测试,此demo遵循以下的设计思路完成。采用C/S架构,配合机作为服务端,创建一个BluetoothServerSocket,使用唯一标识UUID来监听客户端连接请求bluetoothAdapter.listenUsingRfcommWithServiceRecord("MyService", MY_UUID),然后调用serverSocket.accept()阻塞方法进行等待,直到有客户端设备接入为止。一旦有客户端连接成功,accept() 方法会返回一个代表这个连接的 BluetoothSocket 对象。 待测机作为客户端,可以发现周围可用的蓝牙设备,并发起配对,配对成功后在已配对设备列表可以执行连接,调用BluetoothDevice.createRfcommSocketToServiceRecord(MY_UUID)来创建一个BluetoothSocket对象,接着调用这个对象的connect()方法发起连接,这也是一个阻塞式的方法,如果成功方法会正常返回,如果失败则会抛出异常。一旦客户端和服务端连接成功,就可以自由的双向发送和接收数据了。以下是基于此思路实现的具体测试项介绍。

(1)蓝牙延时测试

当已建立起RFCOMM连接,点击蓝牙延时测试项,客户端会立刻开始计时并通过这个链接发送一个极小的数据包,在服务端接收到这个数据后,会不作任何处理立刻返回,客户端接收这个返回的数据包后,终止计时,并计算往返时间(RTT),最终的蓝牙延迟为RTT/2,单位为ms。

(2)蓝牙吞吐量测试

采用 “Echo回声” 测试法。客户端发送一个大尺寸的数据块(例如256KB),服务端的职责不变,仍然是简单地将收到的所有数据原样反弹回来。

客户端操作:点击“蓝牙吞吐量测试”按钮后:

a.客户端准备一个256KB的数据包。

b.记录一个开始时间戳,然后将数据包完整地发送出去。

c.客户端进入接收模式,等待并接收从服务端反弹回来的整整256KB数据。

d.一旦接收完毕,记录一个结束时间戳。

结果计算与显示:程序会计算出整个往返过程所花费的时间,并由此计算出单向的有效传输速率。最终结果会以 KB/s (千字节每秒) 为单位,显示在右侧的“测试结果”区域中。

(3)蓝牙数据完整性测试

使用行业标准的 CRC32 (循环冗余校验) 算法来验证数据。

客户端操作:点击“蓝牙数据完整性测试”按钮后:

a.客户端在内存中创建一个 1KB (1024字节) 的数据包。

b.使用 java.util.zip.CRC32 库,计算出这个原始数据包的 校验和 (Checksum)。

c.将这个1KB的数据包完整地发送给服务端。

d.客户端进入接收模式,等待并接收从服务端“反弹”回来的整整1KB数据。

e.客户端对接收到的数据包,用同样的CRC32算法,计算一个新的校验和。

f.对比两个校验和。如果它们完全一致,证明数据在“一来一回”的完整链路中没有发生任何损坏。

三、结语

此Demo主要还是基于经典蓝牙API的实现,没有涉及低功耗蓝牙(BLE)的处理,小弟才疏学浅,文章中难免有疏漏之处,如有表述不清或是错漏的部分,还希望各位大佬能够不吝赐教。

源码已上传夸克,感兴趣的朋友可以自行下载阅读。

链接:pan.quark.cn/s/6a3cb0906… 提取码:8Ncd

环境:JetBrains Runtime 21.0.8,Android Gradle Plugin Version 8.13.1,Gradle Version 8.13