Android Things 支持蓝牙和蓝牙低功耗 API。在这篇博文中,我们将使用 Bluetooth LE API 在服务器(Android Things 开发板)和客户端(手机/手表上的 Android 应用程序)之间进行通信。
我们将为我们的真棒(awesomeness)建立一个计数器:每当你感觉真棒(awesomeness,无论什么原因),按一下你的移动设备上的按钮。 一只幸运的猫会移动它的爪子,并增加你的真棒(awesomeness)计数器。
BLE基于一个名为 “通用属性概要文件”(General ATTribute profile,GATT)的规范,该规范定义了如何在服务器和客户端之间传输和接收被称为 “属性” (attributes)的短小数据。
它提到了“概况”(profiles),“服务”(services),“特征” (characteristics) 和 “描述” (descriptors)等概念。
一个配置文件是(一或多个)服务的集合。每个服务(可以被认为是一个行为)可以包含一个或多个封装数据的特征(characteristics)。
为了确保互操作性,Bluetooth SIG(特殊兴趣小组)已经预定义了多个配置文件和服务。想象一下,我们要创建我们自己的键盘设备。 为确保兼容性,我们将不得不通过 GATT 配置文件遵循 HID(人机界面设备):
配置文件主要是一个规范,告诉我们我们将不得不实施哪些服务。要创建我们的自定义键盘,我们将必须实施 3 个强制性服务(HID、电池、设备信息)以及可选的扫描参数服务。
如果我们看过电池服务,它暴露了设备内的电池状态,我们可以看到它嵌入了一个名为电池电量的单一的强制只读特性。这个特性封装了一个介于 0 和 100 之间的 int 值,代表了设备电池的百分比。它也有一个可选的 “通知” (Notify)属性,这意味着客户可以订阅它,当值改变时自动通知。
官方文档 是在 Android 上开始使用蓝牙低功耗的最佳方式。
Google 还提供了 2 个示例项目:
部署这两个项目之后,您将能够扫描 Android Things GATT 服务:
服务和特征由 UUID 唯一标识。这里,Raspberry Pi 3 公开了 3 个服务:通用属性(0x1801),通用访问(0x1800)和当前时间服务(0x1805)。后者有两个特征:当前时间(0x2A2B)和本地时间信息(0x2A0F)
关于自定义GATT服务/特性, 虽然按照蓝牙技术联盟的定义实施服务是推荐的方式,但也可以创建自己的专有服务(我们将在今天这样做)。在某些情况下,这可能是首选的解决方案,但是您将不具有互操作性的好处。
您应该为您的非标准服务和特性使用 128 位随机 UUID。短的 16 位 UUID 仅用于由蓝牙标准定义的服务/特性。
现在我们已经熟悉 BLE 关键概念,我们可以开始实施我们的 GATT 服务器。
我们的 Android Things 项目将公开一个具有两个特征的服务:
AwesomenessCounter
:一个只读的,可通知的属性,表示你到目前为止真棒(Awesomeness)的次数AwesomenessInteractor
:当一个客户端为这个特性写一个值时,该设备应该移动猫的爪子并且增加真棒(Awesomeness)计数器。下面的代码受到 sample-bluetooth-le-gattserver 的启发。如果你需要创建一个 GATT 服务器,你可以使用这个项目作为参考,或者按照官方文档。
我们将定义以下常量
SERVICE_UUID = UUID.fromString("795090c7-420d-4048-a24e-18e60180e23c");
CHARACTERISTIC_COUNTER_UUID = UUID.fromString("31517c58-66bf-470c-b662-e352a6c80cba");
CHARACTERISTIC_INTERACTOR_UUID = UUID.fromString("0b89d2d4-0ea6-4141-86bb-0c5fb91ab14a");
DESCRIPTOR_CONFIG_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
如前所述,服务和特征由 UUID 唯一标识。由于我们没有实施标准服务,我们使用随机生成的值。
另外请注意 DESCRIPTOR_CONFIG_UUID
常量:每个特征都有价。如果我们以“电池电量”特性为例,它保持从 0 到 100 的值。特性也可以包含一些描述符。描述符定义元数据,如描述和展示信息。
GATT 描述符的一些示例:
Characteristic User Description
(0x2901):提供特征值的文本用户描述。Valid Range
(0x2906):定义特征的范围。Client Characteristic Configuration
(0x2902):定义特性如何由特定客户端配置。如果一个客户想订阅一个特性,所以当一个值发生变化的时候它可以被自动地通知,它应该在 “客户特性配置” (Client Characteristic Configuration)描述符上执行一个写操作来通知它的意图。
在这里,我们定义一个 DESCRIPTOR_CONFIG_UUID
,以便客户端可以订阅 CHARACTERISTIC_COUNTER_UUID
的值。
首先,我们声明 BLUETOOTH
和 BLUETOOTH_ADMIN
权限。BLUETOOTH_ADMIN
需要启动发现,或自动启用设备上的蓝牙。
<uses-permission android:name="android.permission.BLUETOOTH"></uses-permission>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"></uses-permission>
<uses-feature android:name="android.hardware.bluetooth_le"></uses-feature>
我们还指定我们的应用程序需要使用 bluetooth_le
功能。如果低功耗蓝牙是您的应用程序的可选功能,请将此功能设置为 android:required="false
。
当 Android Things 程序启动时,它应该开始广播(advertising),以便其他设备可以看到它公开哪些 BLE 服务,并可以连接到它。
// The BluetoothAdapter is required for any and all Bluetooth activity.
mBluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
// Some advertising settings. We don't set an advertising timeout
// since our device is always connected to AC power.
AdvertiseSettings settings = new AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
.setConnectable(true)
.setTimeout(0)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
.build();
// Defines which service to advertise.
AdvertiseData data = new AdvertiseData.Builder()
.setIncludeDeviceName(true)
.setIncludeTxPowerLevel(false)
.addServiceUuid(new ParcelUuid(SERVICE_ID))
.build();
// Starts advertising.
mBluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
mBluetoothLeAdvertiser.startAdvertising(settings, data, mAdvertiseCallback);
广播是电池密集型的。在这里,我们的设备总是连接到交流电源,所以它会连续广播。如果它使用电池供电,一个好主意是添加一个超时和一个物理按钮来开始广播过程。 此外,您需要在客户端连接后停止广播。
startAdvertising
方法需要一个 AdvertiseCallback
实例,定义如下:
private AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
Log.i(TAG, "LE Advertise Started.");
}
@Override
public void onStartFailure(int errorCode) {
Log.w(TAG, "LE Advertise Failed: " + errorCode);
}
};
我们必须以编程的方式定义我们的 GATT 服务。请记住,我们的服务应该包含两个特点:
private BluetoothGattService createService() {
BluetoothGattService service = new BluetoothGattService(SERVICE_UUID, SERVICE_TYPE_PRIMARY);
// Counter characteristic (read-only, supports subscriptions)
BluetoothGattCharacteristic counter = new BluetoothGattCharacteristic(CHARACTERISTIC_COUNTER_UUID, PROPERTY_READ | PROPERTY_NOTIFY, PERMISSION_READ);
BluetoothGattDescriptor counterConfig = new BluetoothGattDescriptor(DESCRIPTOR_CONFIG_UUID, PERMISSION_READ | PERMISSION_WRITE);
counter.addDescriptor(counterConfig);
// Interactor characteristic
BluetoothGattCharacteristic interactor = new BluetoothGattCharacteristic(CHARACTERISTIC_INTERACTOR_UUID, PROPERTY_WRITE_NO_RESPONSE, PERMISSION_WRITE);
service.addCharacteristic(counter);
service.addCharacteristic(interactor);
return service;
}
然后,我们用 openGattServer
方法启动低功耗蓝牙服务器。
mGattServer = mBluetoothManager.openGattServer(mContext, mGattServerCallback);
mGattServer.addService(createService());
此方法采用 BluetoothGattServerCallback
实例,该实例包含在读取或写入特征/描述符时实现的回调。
当 GATT 客户端读取 CHARACTERISTIC_COUNTER_UUID
时,我们应该返回计数器的值。为此,我们重写我们的 BluetoothGattServerCallback
的 onCharacteristicReadRequest
方法,如果在计数器特性上有读取请求,则返回 currentCounterValue
:
@Override
public void onCharacteristicReadRequest(BluetoothDevice device,
int requestId, int offset, BluetoothGattCharacteristic characteristic) {
if (CHARACTERISTIC_COUNTER_UUID.equals(characteristic.getUuid())) {
byte[] value = Ints.toByteArray(currentCounterValue);
mGattServer.sendResponse(device, requestId, GATT_SUCCESS, 0, value);
}
}
当 GATT 客户端写入 CHARACTERISTIC_INTERACTOR_UUID
时,我们应该增加计数器的值。为此,我们可以覆写 onCharacteristicWriteRequest
方法:
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device,
int requestId, BluetoothGattCharacteristic characteristic,
boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
if (CHARACTERISTIC_INTERACTOR_UUID.equals(characteristic.getUuid())) {
currentCounterValue++;
notifyRegisteredDevices();
}
}
请注意 notifyRegisteredDevices
被调用了。
由于计数器值已经改变,我们应该通知设备。 稍后我们将看到实现,但首先,我们来处理订阅。
如果客户想要得到计数器特征值的任何变化的通知,就应该把它的意图写在一个配置描述符上。
我们重写 onDescriptorWriteRequest
,并在名为 mRegisteredDevices
的私有列表中保留蓝牙设备的引用:
@Override
public void onDescriptorWriteRequest(BluetoothDevice device,
int requestId, BluetoothGattDescriptor descriptor,
boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
if (DESCRIPTOR_CONFIG_UUID.equals(descriptor.getUuid())) {
if (Arrays.equals(ENABLE_NOTIFICATION_VALUE, value)) {
mRegisteredDevices.add(device);
} else if (Arrays.equals(DISABLE_NOTIFICATION_VALUE, value)) {
mRegisteredDevices.remove(device);
}
if (responseNeeded) {
mGattServer.sendResponse(device, requestId, GATT_SUCCESS, 0, null);
}
}
}
现在,我们可以创建我们的 notifyRegisteredDevices
方法,为每个订阅的设备简单地调用 notifyCharacteristicChanged
:
private void notifyRegisteredDevices() {
BluetoothGattCharacteristic characteristic = mGattServer
.getService(SERVICE_UUID)
.getCharacteristic(CHARACTERISTIC_COUNTER_UUID);
for (BluetoothDevice device : mRegisteredDevices) {
byte[] value = Ints.toByteArray(currentCounterValue);
counterCharacteristic.setValue(value);
mGattServer.notifyCharacteristicChanged(device, characteristic, false);
}
}
我们已经完成了我们的 GATT 服务器的编写。在开始创建客户端之前,我们将首先测试服务器,以确保我们已经正确实施了所有功能。
要测试低功耗蓝牙设备,您可以使用 nRF Connect for Mobile 应用程序。此应用程序允许您扫描蓝牙低功耗设备,并让您读取、写入、订阅特征。
启动应用程序后,我们可以看到 RPI3 是广播。 一旦我们连接到它,我们可以看到我们的自定义服务(UUID = 7950 ...)
使用这个应用程序,我们可以浏览给定服务的所有特征。
我们可以点击阅读按钮(蓝色)来读取计数器特性的值(这里的值等于0x2A [42])。当计数器特性发生变化时,我们也可以得到通知,写在交互器特性上,并看到自动递增的值。
英语原文链接:http://nilhcem.com/android-things/bluetooth-low-energy
观光\评论区