I personally has lots of experience with CoreBluetooth. I also did look at the GATT for sometime so I understand how service, characteristics, descriptor works. This note will focus mainly on implementation and how to make Android device work as peripheral and iOS as central to get data transfer from Android to iOS.
This blog will be updated in the future to finish all its contents.
Android Peripheral
Android peripheral is different a little bit from iOS. First its advertise and GATT service are separates. So let's take about advertise first.
Advertise
We need a BluetoothLeAdvertiser, AdvertiseCallback to do the job. First, we need to check if the device supports Bluetooth Low Energy. Keep in mind that even if the device supports Bluetooth Low Energy, it can still don't support advertise or scan.
So first define class variables.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
private BluetoothLeAdvertiser mBLEAdvertiser; private final AdvertiseCallback mAdvCallback = new AdvertiseCallback() { @Override public void onStartSuccess(AdvertiseSettings settingsInEffect) { super.onStartSuccess(settingsInEffect); Toast.makeText(mContext, "Advertise Start Successfully", Toast.LENGTH_SHORT).show(); Log.d(LOG_TAG, "Advertise Start Successfully"); }
Check if support Bluetooth Low Energy and initialize advertiser.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
public Boolean initializeBTLE() { boolean hasBLE = mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE); if (!hasBLE) { Toast.makeText(mContext, "Bluetooth Low Energy Not Supported On Phone", Toast.LENGTH_SHORT).show(); Log.e(LOG_TAG, "Bluetooth Low Energy Not Supported On Phone."); return false; }
// this will mention later about how to create gatt server return createGattService(); }
Now write the advertise function. Initialize advSetting and advData. We need to pass in a ParcelUuid as service UUID for advertise. This service UUID is for advertise only and has no connection with GATT protocol. Call the startAdvertise and pass in the setting and data we just created and the advertise callback function we created before.
If we ever need to stop the advertise process, this is what we do.
1 2 3 4 5
public void stopAdvertise() { if (mBLEAdvertiser != null && mAdvCallback != null) { mBLEAdvertiser.stopAdvertising(mAdvCallback); } }
GATT Server
Now at this point, the iOS Central should be able to discover the Android Peripheral device. We need to continue setting up a GATT server to make iOS connectable.
So we need a BluetoothManager, a BluetoothGattServer, a BluetoothGattCharacteristic, a BluetoothGattServerCallBack.
For other objects such as BluetoothGATTService etc, we can have them locally in function. The reason that BluetoothGattCharacteristic needs to be a class property is because we need to set value for characteristic when notify the peripheral.
// Setup service and characteristic. addDeviceService(); return true; }
Now for creating service and characteristic, we need to have two more individual uuid for service and characteristic. It should be different from the device uuid we used before for advertise.
If we need peripheral to subscribe characteristic, we need to implement a Descriptor with a specific uuid 00002902-0000-1000-8000-00805F9B34FB.
This uuid is found in Bluetooth SIG as Client Characteristic Configuration. From the Doc, the Assigned Number for that is 0x2902 add base UUID to convert it from 16 bit to 128 bit. Here is the introduction on how to convert.
Moreover we need to give it a write permission and implement the onDescriptorWriteRequest for BluetoothGATTServerCallBack
private void addDeviceService() { // Remove previous Service BluetoothGattService previousService = mGattServer.getService(UUID.fromString(mContext.getString(R.string.service_uuid))); if (previousService != null) { mGattServer.removeService(previousService); }
// Create new service and characteristics mCharacteristic = new BluetoothGattCharacteristic( UUID.fromString(mContext.getString(R.string.characteristic_uuid)), BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);
BluetoothGattDescriptor myDescriptor = new BluetoothGattDescriptor( UUID.fromString(mContext.getString(R.string.notify_descriptor_uuid)), BluetoothGattDescriptor.PERMISSION_WRITE | BluetoothGattDescriptor.PERMISSION_READ); mCharacteristic.addDescriptor(myDescriptor);
mService = new BluetoothGattService( UUID.fromString(mContext.getString(R.string.service_uuid)), BluetoothGattService.SERVICE_TYPE_PRIMARY);
Now we need to implement the BluetoothGattServerCallback. There are couple callbacks we need to handle in order to make it works. First is onDescriptorWriteRequest. We need to call mGattServer.sendResponse(...). That callback gets called when peripheral asked to notify a characteristics and sendResponse tells peripheral that notify success.
1 2 3 4 5 6 7
private final BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() { @Override public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value); mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value); } };
Now the peripheral is able to notify the characteristics and we can send data whatever and whenever we want.
Send Data
When sending data, every time we set value in characteristic and then notify the client. For each notify, we can only put maximum 20 bytes of data into the characteristic. So if we want to send data more than 20 bytes, this is what we do.
We notify it several times and send an "EOF" string to indicate that data has been all sent and on peripheral we can then combine all received data together.
So first I wrote a DataSendManager class. Calling the constructor to pass-in the total data we want to send. And each time we set data in characteristics, we call getNextData() to get the chunk of the data we need to send. If getNextData() returns null, then all data including the "EOF" string has been sent.
class DataSendManager { private final String LOG_TAG = "Data Sender"; private final int MAX_DATA_LENGTH = 20; // bytes private final byte[] EOF = "EOF".getBytes(Charset.forName("UTF-8"));
private byte[] data; private int startIndex; private boolean isFinished;
DataSendManager(String stringToSend) { data = stringToSend.getBytes(Charset.forName("UTF-8")); initializeManager(); }