文章目录
- 科普
- SIG
- 类型
- 制式
- 选择
- 逻辑链路控制适配协议 (L2CAP)
- L2CAP的功能
- 蓝牙框架和 RFCOMM 协议
- 蓝牙安全
- 白名单机制
- 编程
- 蓝牙权限
- Classic Bluetooth
- Bluetooth Low Energy
- 术语
- 角色 & 职能
- 查找 BLE 设备
- 连接设备上的 GATT 服务器
- 绑定服务
- 蓝牙设置
- 连接到设备
- 连接到 GATT 服务
- autoConnect
- 状态广播更新
- Activity 更新
- 关闭 GATT 连接
- 传输 BLE 数据
- 寻找服务
- 读取 BLE 特征
- 接收 GATT 通知
- 蓝牙的UUID是什么?有什么用?
- Characteristic 属性
- 字节序问题
科普
https://zh.wikipedia.org/wiki/%E8%97%8D%E7%89%99
蓝牙(英语:Bluetooth),一种无线通讯技术标准,用来让固定与移动设备,在短距离间交换资料,以形成个人局域网(PAN)。其使用短波特高频(UHF)无线电波,经由 2.4 至 2.485 GHz 的 ISM(工业、科学、医疗)频段来进行通信。1994 年由电信商爱立信(Ericsson)发展出这个技术。它最初的设计,是希望创建一个 RS-232 数据线的无线通信替代版本。它能够链接多个设备,克服同步的问题。
SIG
蓝牙技术联盟(英语:Bluetooth Special Interest Group,缩写为SIG)拥有蓝牙的商标,负责制定蓝牙规范、认证制造厂商,授权他们使用蓝牙技术与蓝牙标志,但本身不负责蓝牙设备的设计、生产及贩售。
类型
Classic Bluetooth(也称作 Bluetooth Basic Rate/Enhanced Data Rate (BR/EDR))、Bluetooth Low Energy(蓝牙 4.0 及更高版本)
EDR:一种更快的 PSK(Phase Shift Keying 相移键控,其他还有幅移键控、频移键控、最小移频键控、高斯滤波最小移频键控、QAM 正交幅度、OFDM 正交频分复用) 调制方案,能够比以前的蓝牙版本快 2 或 3 倍的速度传输数据。
https://www.bluetooth.com/learn-about-bluetooth/tech-overview/
制式
分 Single mode 与 Dual mode。
Single mode 只能与 BT4.0 互相传输无法向下兼容(与 3.0/2.1/2.0 无法相通);Dual mode 可以向下兼容,可与 BT4.0 传输也可以跟 3.0/2.1/2.0 传输
选择
蓝牙 LE 设备(v4 及更高版本)通常向后兼容(可在新版使用)。也就是说,两种类型的蓝牙,经典 (< v4) 和 LE (> v4),彼此完全不同。
经典蓝牙与经典的主从连接一起工作,其中一个设备向另一个设备发送指令,另一个设备服从。
低功耗蓝牙彻底改变了这一点,并用以客户端-服务器为中心的架构取代了该架构。设备采用 GATT 连接的思想,它们具有由服务和特征决定的特定规则和功能。您获得设备的服务通道,挂钩特定特征并读取/写入/订阅来自它的通知。这种新的连接类型允许外围设备仅在被调用时才起作用,从而减少了服务器端不断轮询连接的需要,节省了能源。它还允许您一次连接到多个 BLE 设备。
蓝牙设备被检测为低功耗和具有相同 MAC 地址和名称的普通蓝牙设备
经典蓝牙:蓝牙最初的设计意图,是打电话放音乐。3.0 版本以下的蓝牙,都称为“经典蓝牙”。功耗高、传输数据量大、传输距离只有 10 米。适用于传输音视频等数据量大或需要连续宽带链接的鼠标和其他设备的应用场合。
低功耗蓝牙:就是 BLE,通常说的蓝牙 4.0(及以上版本)。低功耗,数据量小,距离 50 米左右。使用于电池供电、连手机 APP 读取设备信息等应用场合。
逻辑链路控制适配协议 (L2CAP)
逻辑链路控制和适配协议 (L2CAP) 是蓝牙标准中使用的一种协议,它提供更高层和蓝牙堆栈基带层之间的适配。它在主机控制器接口 (HCI) 上方运行,将数据帧从更高层传递到 HCI 或链路管理器。
下图显示了 L2CAP 在蓝牙协议架构中的位置 -
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TabZDDmM-1635124900565)(https://www.tutorialspoint.com/assets/questions/media/38818/link_control_adaptation.jpg)]
L2CAP的功能
- 蓝牙协议栈高层帧与低层帧的适配。
- 支持面向连接和无连接服务。
- 支持基带层的两个链接 -
- 面向使用保留带宽的实时语音流量的同步面向连接 (SCO) 链接。
- 用于尽力而为流量的异步无连接 (ACL) 链接。
- 上层协议的复用,允许它们使用下层提供的链路。
- 对大于下层无线电层容量的上层数据包进行分段和重组。
- 集团管理。
- 上层协议的服务质量 (QoS)。
蓝牙框架和 RFCOMM 协议
蓝牙 RFCOMM(射频通信)协议是一组简单的传输协议,建立在 L2CAP 协议之上,提供仿真 RS-232 串行端口。当不想处理串行接口信令问题时使用封装了 L2CAP 的 RFCOMM。
蓝牙安全
蓝牙安全漏洞与攻击 蓝牙技术的安全漏洞及攻击方法分析
白名单机制
所谓的白名单,就是一组蓝牙地址;
通过白名单,可以只允许特定的蓝牙设备(白名单中列出的)扫描(Scan)、连接(connect)我们;
我们也可以只扫描、连接特定的蓝牙设备(白名单中列出的)。
编程
Bluetooth Classic vs. Bluetooth Low Energy (BLE) on Android – Hints & Implementation Steps
Android-经典蓝牙(BT)-建立长连接传输短消息和文件
BLE与蓝牙科普
Android蓝牙健康设备开发:Health Device Profile(HDP)
Android蓝牙开发—经典蓝牙和BLE(低功耗)蓝牙的区别
Android低功耗蓝牙(BLE)开发的一点感受
【经验】低功耗蓝牙模块如何实现数据传输?
Classic Bluetooth 和 Bluetooth Low Energy 开发有很大的区别。
连接:
Classic Bluetooth 建立连接的方式实际上就是 Socket 的连接的建立,利用搜索找到的 BluetoothDevice,调用其方法 createRfcommSocketToServiceRecord(UUID)。最后,使用获取到的 BluetoothDevice 调用其方法 connect()。
Bluetooth Low Energy 建立连接的方式类似于数据库的连接。通过 BluetoothAdapter 的 getRemoteDevice(address) 方法获取相应 BLE 从设备的 BluetoothDevice,其中的 address 为目标蓝牙设备 MAC 地址。然后通过此 BluetoothDevice 的 connectGatt(this, false, mGattCallback) 方法获取设备连接以及返回属性句柄来访问 Gatt 数据库。此时的连接,只能够进行监听,也就是获取到当前BLE从设备广播出来的数据。
读写:
Classic Bluetooth 使用 BluetoothSocket 的 getOutputStream() 方法获取输出流写入需要发送的数据,调用 BluetoothSocket 的 getInputStream() 方法获取输入流读取。
Bluetooth Low Energy 想要实现主设备对从设备的数据发送,则需要直接读取获取到的从设备的 Characteristic。
蓝牙权限
BLUETOOTH
:(必需),例如请求连接、接受连接和传输数据。
BLUETOOTH_ADMIN
:(可选),如果您希望您的应用程序启动设备发现或操作蓝牙设置、创建套接字连接,除了 BLUETOOTH 权限之外,您还必须声明此权限。大多数应用程序作为连接的主动方需要此权限仅用于发现本地蓝牙设备。
ACCESS_FINE_LOCATION
:(必需),因为蓝牙扫描需要收集有关用户位置的信息。该信息可能来自用户自己的设备,以及在商店和交通设施等位置使用的蓝牙信标。
由于 ACCESS_FINE_LOCATION
是 危险权限,你需要在清单中声明它并 在运行时请求此权限。
Note: Android 9(API 级别 28)或更低版本,需要声明
ACCESS_COARSE_LOCATION
而不是ACCESS_FINE_LOCATION
权限。
Note: Android 10 及更高版本,需要拥有ACCESS_BACKGROUND_LOCATION
权限才能发现蓝牙设备。有关此要求的更多信息,请参阅 后台访问位置。
使用if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
条件判断该版本。
Note: Android 8.0(API 级别 26)及更高版本,可以使用 CompanionDeviceManager 对附近的配套设备执行扫描,而无需位置权限。有关此选项的更多信息,请参阅 配套设备配对。
如果您希望您的应用程序启动设备发现或操作蓝牙设置,除了 BLUETOOTH 权限之外,您还必须声明 BLUETOOTH_ADMIN 权限。
Classic Bluetooth
https://developer.android.com/guide/topics/connectivity/bluetooth
https://developer.android.com/guide/topics/connectivity/bluetooth?hl=zh-cn
Bluetooth Low Energy
蓝牙低功耗概览
低功耗蓝牙
Android BluetoothLeGatt Sample
The Ultimate Guide to Android Bluetooth Low Energy:Android 低功耗蓝牙终极指南!
术语
Generic Attribute Profile (GATT):通用属性配置文件 是解释如何在 BLE 链路发送和接收“属性”短数据段的通用规范。当前所有的 BLE 应用配置文件都是基于 GATT 通过“属性”进行交流。一个设备可以实现多个服务的配置文件。例如,一台设备可能包含心率监测仪和电池电量检测器。
Attribute Protocol (ATT):GATT 建立在属性协议 (ATT) 之上,二者的关系也被称为 GATT/ATT。该协议为在 BLE 设备上运行进行优化,使用尽可能少的字节。每个属性均由通用唯一标识符 (UUID) 进行唯一标识,按照 Service - Characteristic 的格式传输。
Characteristic:特征 包含一个特征值以及 0-n 个描述此特征值的 Descriptor,相当于一个类。
Descriptor:描述符 是已定义的描述特征值的属性。例如,描述符可指定特征值的可接受范围或特定于特征值的度量单位。
Service:服务 是特征的集合。例如,在名为“心率监测器”的服务中包括“心率测量”等特征。在 bluetooth.org 上可查看现有的 GATT 配置文件及服务的列表。
角色 & 职能
在 BLE 连接中,中央(主)设备进行扫描、寻找广播;外围(从)设备发出广播。
在 BLE 数据传输中,发送数据的设备充当 Gatt 服务器,接收数据的设备充当 Gatt 客户端。
查找 BLE 设备
使用 startLeScan() 方法,将 BluetoothAdapter.LeScanCallback 作为参数。必须实现此回调。扫描非常耗电,要设置扫描时间限制;找到所需设备后,立即停止扫描。
以下代码段展示如何启动和停止扫描:
/*** Activity for scanning and displaying available BLE devices.*/
public class DeviceScanActivity extends ListActivity {private BluetoothAdapter bluetoothAdapter;private boolean mScanning;private Handler handler;// Stops scanning after 10 seconds.private static final long SCAN_PERIOD = 10000;...private void scanLeDevice(final boolean enable) {if (enable) {// Stops scanning after a pre-defined scan period.handler.postDelayed(new Runnable() {@Overridepublic void run() {mScanning = false;bluetoothAdapter.stopLeScan(leScanCallback);}}, SCAN_PERIOD);mScanning = true;bluetoothAdapter.startLeScan(leScanCallback);} else {mScanning = false;bluetoothAdapter.stopLeScan(leScanCallback);}...}
...
}
Note: The BluetoothLeScanner is only available from the BluetoothAdapter if Bluetooth is currently enabled on the device. If Bluetooth is not enabled, then getBluetoothLeScanner() returns null.
以下代码示例是 ScanCallback 的实现,它是用于传递 BLE 扫描结果的接口。找到结果后,会将它们添加到 DeviceScanActivity 中的列表适配器中以显示给用户。
private LeDeviceListAdapter leDeviceListAdapter = new LeDeviceListAdapter();// Device scan callback.
private ScanCallback leScanCallback =new ScanCallback() {@Overridepublic void onScanResult(int callbackType, ScanResult result) {super.onScanResult(callbackType, result);leDeviceListAdapter.addDevice(result.getDevice());leDeviceListAdapter.notifyDataSetChanged();}};
Note: You can only scan for Bluetooth LE devices or scan for classic Bluetooth devices, as described in Bluetooth overview. You cannot scan for both Bluetooth LE and classic devices at the same time.
连接设备上的 GATT 服务器
绑定服务
在本例中,提供一个 Activity (DeviceControlActivity) 来连接、显示设备及服务数据。真正通过 API 与 BLE 设备交互的是名为 BluetoothLeService 的 Service:
class BluetoothLeService extends Service {private Binder binder = new LocalBinder();@Nullable@Overridepublic IBinder onBind(Intent intent) {return binder;}class LocalBinder extends Binder {public BluetoothLeService getService() {return BluetoothLeService.this;}}
}
class DeviceControlActivity extends AppCompatActivity {private BluetoothLeService bluetoothService;private ServiceConnection serviceConnection = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {bluetoothService = ((LocalBinder) service).getService();if (bluetoothService != null) {// call functions on service to check connection and connect to devices}}@Overridepublic void onServiceDisconnected(ComponentName name) {bluetoothService = null;}};@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.gatt_services_characteristics);Intent gattServiceIntent = new Intent(this, BluetoothLeService.class);bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE);}
}
蓝牙设置
是否支持蓝牙可用 BluetoothAdapter 判断,如果 getDefaultAdapter() 返回 null,则该设备不支持蓝牙。此逻辑包装在一个 initialize() 函数中,Activity 在其 ServiceConnection 实现中调用此函数。
class BluetoothLeService extends Service {public static final String TAG = "BluetoothLeService";private BluetoothAdapter bluetoothAdapter;public boolean initialize() {bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();if (bluetoothAdapter == null) {Log.e(TAG, "Unable to obtain a BluetoothAdapter.");return false;}return true;}...
}
class DeviceControlsActivity extends AppCompatActivity {private ServiceConnection serviceConnection = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {bluetoothService = ((LocalBinder) service).getService();if (bluetoothService != null) {if (!bluetoothService.initialize()) {Log.e(TAG, "Unable to initialize Bluetooth");finish();}// perform device connection}}@Overridepublic void onServiceDisconnected(ComponentName name) {bluetoothService = null;}};...
}
不支持蓝牙时退出应用,做好用户提示。
BluetoothAdapter 的 isEnabled() 判断蓝牙是否开启。如果未开启,可以主动通过代码引导用户开启,或者被动监听蓝牙状态的广播,确保之后的操作中蓝牙可以使用。
- 以编程方式启用它 BluetoothAdapter.getDefaultAdapter().enable()
- startActivityForResult() 传递 ACTION_REQUEST_ENABLE 意图可显示对话框,通过系统设置开启蓝牙(无需停止您的应用)。
- 上述方法还需要在 onActivityResult() 中检查返回值,直接 startActivity() 启动可检测性 则无需此步骤。
注册广播监听器添加 ACTION_STATE_CHANGED 意图,监听蓝牙启闭状态。
连接到设备
一旦 BluetoothService 被初始化,它就可以连接到 BLE 设备。活动需要将 BLE 设备地址发送到服务,以便它可以启动连接。该服务将首先调用 BluetoothAdapter 上的 getRemoteDevice()
来访问设备。如果适配器无法找到具有该地址的设备,getRemoteDevice() 将抛出 IllegalArgumentException。
有效的蓝牙硬件地址必须大写,格式如“00:11:22:33:AA:BB”。BluetoothAdapter#checkBluetoothAddress(String) 可用于验证蓝牙地址。
public boolean connect(final String address) {if (bluetoothAdapter == null || address == null) {Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");return false;}try {final BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);} catch (IllegalArgumentException exception) {Log.w(TAG, "Device not found with provided address.");return false;}// connect to the GATT server on the device
}
一旦服务被初始化,DeviceControlActivity 就会调用这个 connect() 函数。在以下示例中,设备地址作为额外意图由 DeviceScanActivity 传递过来。
private ServiceConnection serviceConnection = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {bluetoothService = ((LocalBinder) service).getService();if (bluetoothService != null) {if (!bluetoothService.initialize()) {Log.e(TAG, "Unable to initialize Bluetooth");finish();}// perform device connectionbluetoothService.connect(deviceAddress);}}@Overridepublic void onServiceDisconnected(ComponentName name) {bluetoothService = null;}
};
连接到 GATT 服务
一旦服务连接到设备,则需要进一步连接到 BLE 设备上的 GATT 服务器。使用 connectGatt() 方法。此方法采用三个参数:一个 Context 对象、autoConnect(布尔值,指示是否在可用时自动连接到 BLE 设备),以及对 BluetoothGattCallback 的引用。BluetoothGattCallback 来接收有关连接状态、服务发现、特征读取和特征通知的通知。该方法返回 BluetoothGatt 实例,然后您可使用该实例执行 GATT 客户端操作,例如,不再需要时关闭连接。。
在此示例中,应用程序直接连接到 BLE 设备,因此为 autoConnect 传递 false。
class BluetoothService extends Service {...private BluetoothGatt bluetoothGatt;...public boolean connect(final String address) {if (bluetoothAdapter == null || address == null) {Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");return false;}try {final BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);// connect to the GATT server on the devicebluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback);return true;} catch (IllegalArgumentException exception) {Log.w(TAG, "Device not found with provided address. Unable to connect.");return false;}}
}
private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {@Overridepublic void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {if (newState == BluetoothProfile.STATE_CONNECTED) {// successfully connected to the GATT Server} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {// disconnected from the GATT Server}}
};
autoConnect
直接连接意味着手机将尝试连接到设备 30 秒,扫描间隔为 60 毫秒,窗口为 30 毫秒。这意味着,如果您发布广告,则有 50% 的几率会捕获广告事件并建立连接。一旦设备稍后断开连接(断开连接或从站断开连接),手机将不会再次尝试重新连接。
自动连接适用于当设备进入范围时应自动连接的后台连接。它使用 1280 毫秒的扫描间隔和 30 毫秒的扫描窗口,这意味着它将以 ~2.34% 的几率捕获广告事件。因此,如果您在外围设备上以 1 秒的间隔播发广告,则建立连接的预期时间为 43 秒。当设备稍后在连接后断开连接(断开连接或从站断开连接)时,手机将再次尝试重新连接。
请注意,在 BluetoothGatt 对象上执行 connect() 方法(不带参数),将使用自动连接过程。
请注意,您一次只能有一次未完成的直接连接尝试,但在 Android 6.0.1 中最多只能有 10 次自动连接尝试未完成。
对于具有用于连接到外围设备的按钮的用户界面,直接连接方法是最佳使用方法,因为它的连接速度更快,并且还具有超时。对于后台连接,应使用自动连接。
https://issuetracker.google.com/issues/37071781
如果从 onConnectionStateChange() 获得状态 133,则表示连接尝试超时。如果我没记错的话,10 秒后的直连(autoConnect = false)就会发生这种情况。
使用 BLE,有两种方法可以创建连接,使用“LE 创建连接”命令直接连接到设备,或者将一个或多个设备添加到白名单并请求连接到白名单。在 autoConnect=false(他们称之为“直接”)时使用前一种方法,当 autoConnect=true 时使用后者(他们称之为“背景”)。
如果“直接”连接未完成,则“直接”连接将在短时间内以 status=133 超时,缓解方法是在失败后调用 BluetoothGatt 上的 connect(),这会导致后台连接不会超时或具有更长的超时时间。
https://issuetracker.google.com/issues/36995652
直接连接和自动连接之间存在一些未记录在案的差异:
直接连接是具有 30 秒超时的连接尝试。当直接连接正在进行时,它将暂停所有当前的自动连接。如果已经有一个直接连接挂起,则最后一个直接连接不会立即执行,而是排队并在前一个完成时开始。
使用自动连接,您可以同时拥有多个挂起的连接,并且它们永远不会超时(直到明确中止或蓝牙关闭)。
如果连接是通过自动连接建立的,当远程设备断开连接时,Android 将自动尝试重新连接到远程设备,直到您手动调用 disconnect() 或 close()。一旦通过直接连接建立的连接断开,就不会尝试重新连接到远程设备。
与自动连接相比,直接连接具有不同的扫描间隔和扫描窗口,其占空比更高,这意味着它将投入更多的无线电时间来侦听远程设备的可连接广告,即连接建立得更快。
Android 10 的新变化
从 Android 10 开始,直接连接队列被移除,并且不会再暂时暂停自动连接。这是因为直接连接现在像自动连接一样使用白名单。当直接连接正在进行时,扫描窗口/间隔得到改进。
https://stackoverflow.com/questions/40156699/which-correct-flag-of-autoconnect-in-connectgatt-of-ble
自动连接仅适用于缓存或绑定设备!但是,重新启动手机或打开/关闭蓝牙将清除缓存,因此您必须在使用自动连接之前执行必要的检查!真的很烦…
Making Android BLE work — part 2
蓝牙设备地址
由于一个可怕的设计缺陷,不可能通过完整地址(地址类型随机/公共+地址)告诉蓝牙堆栈连接。当您想要获取BluetoothDevice对象(然后用于连接)时,您只能提供 48 位地址。通过挖掘 Android 蓝牙堆栈的源代码,发现蓝牙设备由 48 位地址而不是 49 位地址标识的问题无处不在。他们的“快速修复”是向设备信息添加一个属性,指示设备是否具有公共地址或随机地址。该位不能由应用程序设置,只能在扫描期间设置。连接时connectGatt并且地址类型未知,它将尝试使用公共地址类型建立(Android 7 的一些子版本在使用“自动连接”时根据 48 位地址中的某些位对地址类型进行了一些愚蠢的猜测)。如果您的 BLE 设备具有静态随机地址,则它不会连接。通过执行扫描并检测到您的设备,它会将设备的地址和地址类型存储在 RAM 中的表中,因此如果您稍后使用 连接到它connectGatt,它将成功,因为现在使用了正确的地址类型。当蓝牙重新启动时,该表被清除。请注意,如果您执行绑定,则设备信息将写入磁盘,包括地址类型,因此即使蓝牙重新启动,连接到绑定设备也应始终有效。
BLE (Bluetooth low energy) on android, create and reconnect to device which is not always present
状态广播更新
该服务声明了一个广播新状态的函数,接收动作字符串参数形成 Intent 向系统发送广播。
private void broadcastUpdate(final String action) {final Intent intent = new Intent(action);sendBroadcast(intent);
}
在 BluetoothGattCallback 中使用以发送有关与 GATT 服务器的连接状态的信息。
class BluetoothService extends Service {public final static String ACTION_GATT_CONNECTED ="com.example.bluetooth.le.ACTION_GATT_CONNECTED";public final static String ACTION_GATT_DISCONNECTED ="com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";private static final int STATE_DISCONNECTED = 0;pprivate static final int STATE_CONNECTED = 2;private int connectionState;...private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {@Overridepublic void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {if (newState == BluetoothProfile.STATE_CONNECTED) {// successfully connected to the GATT ServerconnectionState = STATE_CONNECTED;broadcastUpdate(ACTION_GATT_CONNECTED);} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {// disconnected from the GATT ServerconnectionState = STATE_DISCONNECTED;broadcastUpdate(ACTION_GATT_DISCONNECTED);}}};…
}
Activity 更新
通过侦听来自服务的事件,活动能够根据与 BLE 设备的当前连接状态更新用户界面。
class DeviceControlsActivity extends AppCompatActivity {...private final BroadcastReceiver gattUpdateReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {final String action = intent.getAction();if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {connected = true;updateConnectionState(R.string.connected);} else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {connected = false;updateConnectionState(R.string.disconnected);}}};@Overrideprotected void onResume() {super.onResume();registerReceiver(gattUpdateReceiver, makeGattUpdateIntentFilter());if (bluetoothService != null) {final boolean result = bluetoothService.connect(deviceAddress);Log.d(TAG, "Connect request result=" + result);}}@Overrideprotected void onPause() {super.onPause();unregisterReceiver(gattUpdateReceiver);}private static IntentFilter makeGattUpdateIntentFilter() {final IntentFilter intentFilter = new IntentFilter();intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED);intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED);return intentFilter;}
}
在传输 BLE 数据中,BroadcastReceiver 还用于通知寻找服务以及读取特征数据的结果。
关闭 GATT 连接
当活动与服务解除绑定时,连接将关闭以避免耗尽设备电池。
class BluetoothService extends Service {...@Overridepublic boolean onUnbind(Intent intent) {close();return super.onUnbind(intent);}private void close() {if (bluetoothGatt == null) {Return;}bluetoothGatt.close();bluetoothGatt = null;}
}
传输 BLE 数据
Making Android BLE work — part 3:BluetoothGatt 一次只能执行 1 个异步操作的原理 & 编写简单的队列执行 BluetoothGatt 操作
连接到 BLE GATT 服务器后,您可以使用该连接来找出设备上可用的服务、从设备查询数据以及在某个 GATT 特性发生变化时请求通知。
寻找服务
连接到 BLE 设备上的 GATT 服务器后要做的第一件事就是执行服务发现。在以下示例中,一旦服务成功连接到设备,discoverServices() 函数会从 BLE 设备查询信息。
该服务需要覆盖 BluetoothGattCallback 中的 onServicesDiscovered() 函数。当设备报告其可用服务时调用此函数。
class BluetoothService extends Service {public final static String ACTION_GATT_SERVICES_DISCOVERED ="com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";...private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {@Overridepublic void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {if (newState == BluetoothProfile.STATE_CONNECTED) {// successfully connected to the GATT ServerconnectionState = STATE_CONNECTED;broadcastUpdate(ACTION_GATT_CONNECTED);// Attempts to discover services after successful connection.bluetoothGatt.discoverServices();} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {// disconnected from the GATT ServerconnectionState = STATE_DISCONNECTED;broadcastUpdate(ACTION_GATT_DISCONNECTED);}}@Overridepublic void onServicesDiscovered(BluetoothGatt gatt, int status) {if (status == BluetoothGatt.GATT_SUCCESS) {broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);} else {Log.w(TAG, "onServicesDiscovered received: " + status);}}};
}
该服务使用广播来通知活动。一旦发现了服务,服务就可以调用 getServices() 来获取报告的数据。
class BluetoothService extends Service {...public List<BluetoothGattService> getSupportedGattServices() {if (bluetoothGatt == null) return null;return bluetoothGatt.getServices();}
}
Activity 可以在收到广播 Intent 时调用此函数,表明服务发现已经完成。
class DeviceControlsActivity extends AppCompatActivity {...private final BroadcastReceiver gattUpdateReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {final String action = intent.getAction();if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {connected = true;updateConnectionState(R.string.connected);} else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {connected = false;updateConnectionState(R.string.disconnected);} else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {// Show all the supported services and characteristics on the user interface.displayGattServices(bluetoothService.getSupportedGattServices());}}};
}
读取 BLE 特征
一旦您的应用程序连接到 GATT 服务器并发现服务,它就可以在支持的情况下读取和写入属性。例如,以下代码段遍历服务器的服务和特征并将它们显示在 UI 中:
public class DeviceControlActivity extends Activity {...// Demonstrates how to iterate through the supported GATT// Services/Characteristics.// In this sample, we populate the data structure that is bound to the// ExpandableListView on the UI.private void displayGattServices(List<BluetoothGattService> gattServices) {if (gattServices == null) return;String uuid = null;String unknownServiceString = getResources().getString(R.string.unknown_service);String unknownCharaString = getResources().getString(R.string.unknown_characteristic);ArrayList<HashMap<String, String>> gattServiceData =new ArrayList<HashMap<String, String>>();ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData= new ArrayList<ArrayList<HashMap<String, String>>>();mGattCharacteristics =new ArrayList<ArrayList<BluetoothGattCharacteristic>>();// Loops through available GATT Services.for (BluetoothGattService gattService : gattServices) {HashMap<String, String> currentServiceData =new HashMap<String, String>();uuid = gattService.getUuid().toString();currentServiceData.put(LIST_NAME, SampleGattAttributes.lookup(uuid, unknownServiceString));currentServiceData.put(LIST_UUID, uuid);gattServiceData.add(currentServiceData);ArrayList<HashMap<String, String>> gattCharacteristicGroupData =new ArrayList<HashMap<String, String>>();List<BluetoothGattCharacteristic> gattCharacteristics =gattService.getCharacteristics();ArrayList<BluetoothGattCharacteristic> charas =new ArrayList<BluetoothGattCharacteristic>();// Loops through available Characteristics.for (BluetoothGattCharacteristic gattCharacteristic :gattCharacteristics) {charas.add(gattCharacteristic);HashMap<String, String> currentCharaData =new HashMap<String, String>();uuid = gattCharacteristic.getUuid().toString();currentCharaData.put(LIST_NAME, SampleGattAttributes.lookup(uuid,unknownCharaString));currentCharaData.put(LIST_UUID, uuid);gattCharacteristicGroupData.add(currentCharaData);}mGattCharacteristics.add(charas);gattCharacteristicData.add(gattCharacteristicGroupData);}...}
...
}
GATT 服务器提供您可以从设备读取的特性列表。为了查询数据,调用BluetoothGatt上的readCharacteristic()函数,传入你要读取的BluetoothGattCharacteristic。
class BluetoothService extends Service {...public void readCharacteristic(BluetoothGattCharacteristic characteristic) {if (bluetoothGatt == null) {Log.w(TAG, "BluetoothGatt not initialized");return;}bluetoothGatt.readCharacteristic(characteristic);}
}
在这个例子中,服务实现了一个函数来调用 readCharacteristic()。这是一个异步调用。结果被发送到BluetoothGattCallback 函数onCharacteristicRead()。
class BluetoothService extends Service {...private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {...@Overridepublic void onCharacteristicRead(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic,int status) {if (status == BluetoothGatt.GATT_SUCCESS) {broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);}}};
}
重载 broadcastUpdate 函数将特征读取为原来类型(蓝牙属于底层数据传输,所以实际开发更多发送的是16进制数据)。请注意,本节中的数据解析是根据蓝牙心率测量配置文件规范执行的。
private void broadcastUpdate(final String action,final BluetoothGattCharacteristic characteristic) {final Intent intent = new Intent(action);// This is special handling for the Heart Rate Measurement profile. Data// parsing is carried out as per profile specifications.if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {int flag = characteristic.getProperties();int format = -1;if ((flag & 0x01) != 0) {format = BluetoothGattCharacteristic.FORMAT_UINT16;Log.d(TAG, "Heart rate format UINT16.");} else {format = BluetoothGattCharacteristic.FORMAT_UINT8;Log.d(TAG, "Heart rate format UINT8.");}final int heartRate = characteristic.getIntValue(format, 1);Log.d(TAG, String.format("Received heart rate: %d", heartRate));intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));} else {// For all other profiles, writes the data formatted in HEX.final byte[] data = characteristic.getValue();if (data != null && data.length > 0) {final StringBuilder stringBuilder = new StringBuilder(data.length);for(byte byteChar : data)stringBuilder.append(String.format("%02X ", byteChar));intent.putExtra(EXTRA_DATA, new String(data) + "\n" +stringBuilder.toString());}}sendBroadcast(intent);
}
接收 GATT 通知
BLE 应用程序通常会要求设备在特征发生变化时的发送通知。在以下示例中,该服务实现 setCharacteristicNotification() 方法:
class BluetoothService extends Service {...public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,boolean enabled) {if (bluetoothGatt == null) {Log.w(TAG, "BluetoothGatt not initialized");Return;}bluetoothGatt.setCharacteristicNotification(characteristic, enabled);// This is specific to Heart Rate Measurement.if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);bluetoothGatt.writeDescriptor(descriptor);}}
}
为特征启用通知后,如果远程设备上的特征发生更改,则会触发 onCharacteristicChanged() 回调:
class BluetoothService extends Service {...private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {...@Overridepublic void onCharacteristicChanged(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic) {broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);}};
}
蓝牙的UUID是什么?有什么用?
UUID(Universally Unique Identifier) 统一表示定义
蓝牙MAC相当于TCP的IP,蓝牙UUID相当于TCP的端口。
蓝牙是通过串口发送 AT 命令,蓝牙默认是在数据模式的,要配置为 AT 命令模式,对其进行设置,不过 UUID 在出厂前是设置过的。
对于蓝牙设备,每个服务都有一个与它对应的UUID(唯一的),如:
SPP devices:00001101-0000-1000-8000-00805F9B34FB
信息同步服务:00001104-0000-1000-8000-00805F9B34FB
文件传输服务:00001106-0000-1000-8000-00805F9B34FB
val deviceNameUUID = UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb")
val appearanceUUID = UUID.fromString("00002a01-0000-1000-8000-00805f9b34fb")
val batteryLevelUUID = UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb")
val modelNumberUUID = UUID.fromString("00002A24-0000-1000-8000-00805f9b34fb")
val serialNumberUUID = UUID.fromString("00002A25-0000-1000-8000-00805f9b34fb")
val firmwareRevisionUUID = UUID.fromString("00002A26-0000-1000-8000-00805f9b34fb")
val hardwareRevisionUUID = UUID.fromString("00002A27-0000-1000-8000-00805f9b34fb")
val softwareRevisionUUID = UUID.fromString("00002A28-0000-1000-8000-00805f9b34fb")
val manufacturerNameUUID = UUID.fromString("00002A29-0000-1000-8000-00805f9b34fb")
蓝牙之数据传输问题
Characteristic 属性
read 和 write。
BLE INTRODUCTION: NOTIFY OR INDICATE
Notify 和 Indicate 有什么区别?
Indicate 需要确认,而 Notify 不需要。
默认情况下,只要您的低功耗蓝牙 (BLE) 设备有新数据要发布,您就无法将数据推送到远程客户端。如果您阅读我之前的文章,您就会知道您需要启用适当的权限并包含一个“客户端特征配置描述符”(CCCD)才能实现。不要忘记,您的远程客户端必须通过 CCCD 订阅该属性才能接收推送的数据。
通常,当人们希望他们的远程客户端在他们的 BLE 设备有新数据时异步接收更新时,他们会推送数据。但是,因为 Notify 是未确认的,所以它是不可靠的,因为您将不知道您的远程客户端是否已收到数据。
为了解决这个问题,让我介绍一下 Indicate 功能。它与 Notify 几乎相同,只是它支持确认。如果您的远程客户端收到数据,则必须发送确认。但是,这种可靠性是以牺牲速度为代价的,因为您的 BLE 设备将等待确认直到超时。
字节序问题
GATT 字段总是(或至少应该总是)小端。这在蓝牙核心规范中进行了讨论。
来自规范第 3 卷,G 部分(涵盖 GATT):
2.4 配置文件基础
…
• GATT Profile 内的多八位字节字段应首先发送最低有效八位字节(小端)。
阅读本规范时要非常小心,因为有些部分是按网络顺序(大端)排列的,但 GATT 属性总是应该是小端的。
(我说“应该永远是”的唯一原因是蓝牙设备的一个规则是你总会在该领域找到一些违反规则的设备…但规范很明确。)
Byte order of multiple fields in one BLE characteristic