This guide describes how to create your own peripheral using the BluetoothPeripheralManager
class. This class has been designed to make it as easy as possible to develop your own peripheral. An example project is available.
The first thing to do is to create an instance of the BluetoothPeripheralManager
class:
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothPeripheralManager peripheralManager = new BluetoothPeripheralManager(context, bluetoothManager, peripheralManagerCallback);
Not all Android phones support creating a peripheral. Most recent phones support it but some older ones may not. So make sure you do a check like this:
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (!bluetoothAdapter.isMultipleAdvertisementSupported()) {
Timber.e("not supporting advertising");
}
Now that you created your BluetoothPeripheralManager
you need to add some services. Which services you add is up to you of course.
Setting up a heartrate service could be done like this:
UUID HRS_SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb");
UUID HEARTRATE_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb");
BluetoothGattService service = new BluetoothGattService(HRS_SERVICE_UUID, SERVICE_TYPE_PRIMARY);
BluetoothGattCharacteristic measurement = new BluetoothGattCharacteristic(HEARTRATE_MEASUREMENT_CHARACTERISTIC_UUID, PROPERTY_READ | PROPERTY_INDICATE, PERMISSION_READ);
service.addCharacteristic(measurement);
peripheralManager.add(service);
The add()
method is asynchronous and you will receive a callback via the BluetoothPeripheralManagerCallback
class on the method onServiceAdded()
. The add()
call is enqueued so you can call it several times in a row.
To start advertising you call the method startAdvertising
with the advertise settings, advertise data and scan response. Here is an example:
AdvertiseSettings advertiseSettings = new AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
.setConnectable(true)
.setTimeout(0)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
.build();
AdvertiseData advertiseData = new AdvertiseData.Builder()
.setIncludeTxPowerLevel(true)
.addServiceUuid(new ParcelUuid(serviceUUID))
.build();
AdvertiseData scanResponse = new AdvertiseData.Builder()
.setIncludeDeviceName(true)
.build();
peripheralManager.startAdvertising(advertiseSettings, scanResponse, advertiseData);
When a remote central connects and tries to read a characteristic, you get a callback on onCharacteristicRead
.
You need to return a ReadResponse object which must contain a GattStatus object and optionally a byte array.
If you want to reject the read, just return a GattStatus that is not SUCCESS. If you are returning SUCCESS you must also provide a byte array with the value of the characteristic.\
@Override
public ReadResponse onCharacteristicRead(@NotNull BluetoothCentral central, @NotNull BluetoothGattCharacteristic characteristic) {
return new ReadResponse(GattStatus.SUCCESS, getCurrentTime());
}
When a write request happens, you get a callback on onCharacteristicWrite
. If you want to validate the value before the write is completed you can do that by overriding this method. If you consider the value valid, you must return GattStatus.SUCCESS and otherwise you return some other GattStatus value that represents the error.
After you return GattStatus.SUCCESS, the value is assigned to the characteristic (only when api-level is < 33). Otherwise the characteristics's value will remain unchanged and the remote central will receive an error. For example:
@Override
public GattStatus onCharacteristicWrite(@NotNull BluetoothCentral central, @NotNull BluetoothGattCharacteristic characteristic, @NotNull byte[] value) {
if (isValid(value, characteristic)) {
return GattStatus.SUCCESS;
} else {
// Return an error, typical INVALID_ATTRIBUTE_VALUE_LENGTH or VALUE_NOT_ALLOWED
return GattStatus.VALUE_NOT_ALLOWED;
}
}
Read or write request for descriptors work in the same way as for characteristics. The only exception is when the descriptor happens to be the CCC descriptor, which is used to turn on/off notifications
If you have a characteristic with PROPERTY_INDICATE or PROPERTY_NOTIFY and a CCC descriptor added, then a remote central may 'enable notifications'. Blessed will doublecheck if the the correct descriptor values are written, and if correct it will call either onNotifyingEnabled
or onNotifyingDisabled
. It is then your responsibility to actually follow up and do the notifications.
@Override
public void onNotifyingEnabled(@NotNull BluetoothCentral central, @NotNull BluetoothGattCharacteristic characteristic) {
if (characteristic.getUuid().equals(CURRENT_TIME_CHARACTERISTIC_UUID)) {
notifyCurrentTime();
}
}
@Override
public void onNotifyingDisabled(@NotNull BluetoothCentral central, @NotNull BluetoothGattCharacteristic characteristic) {
if (characteristic.getUuid().equals(CURRENT_TIME_CHARACTERISTIC_UUID)) {
stopNotifying();
}
}
Once notifications have been enabled, you can send notifications by calling:
peripheralManager.notifyCharacteristicChanged(value, characteristic);
Note that you have to pass the value for the characteristic. That is there so that you can do high speed notifications as each call to notifyCharacteristicChanged() will be queued up. So you can call this function in a loop and then each command is executed the value of the characteristic will be updated and sent to the remote central.
When a remote central connects or disconnects, the following callbacks are called:
@Override
public void onCentralConnected(@NotNull BluetoothCentral central) {
// Do something, e.g. initialization of characteristics
}
@Override
public void onCentralDisconnected(@NotNull BluetoothCentral central) {
if (noCentralsConnected()) {
stopNotifying();
}
}
Typically, when a central disconnects, you stop notifying and clean up.
The BluetoothPeripheralManager class supports long reads and writes. It will take care of splitting up characteristic byte arrays in smaller chunks and re-assembling them. Hence, nothing special is needed and they function the same way as normal read and writes.
If you use the BluetoothCentralManager and BluetoothPeripheralManager at the same time, you need to tell the peripheralmanager who the central manager is:
peripheralManager.setCentralManager(central)
If you don't do this, the peripheral manager will not be able to distinguish centrals from peripherals and you will see too many connected events.