Blutooth iBeacon Part 3: Gatt Service
In past part, you can separate the your ibeacons from a group of bluetooth devices. Are you exciting? Let's connect to the beacon and do some change. In new activity, called DeviceControlActivity.java, we need a service to chat with the Gatt server in the beacon.
The Gatt server handles all the actions and callbacks. And it only listens to the device who carried the correct pass or UUID. Otherwise, Ble is just a one way broadcast device.
In our service, we have, 1. bind(ON) and unbind(OFF); 2. connect and disconnect; 3. callback and broadcast update; 4. read and write. Not bad, they are easy to remember in four pairs. That's all we need to plan this service. Let's call yourNameBleService.java. Mine is HomanBleService.
Before we execute our plan, we need variables. The first part is action strings.
public static final String ACTION_DATA_AVAILABLE =
"ACTION_DATA_AVAILABLE";
public static final String ACTION_GATT_CONNECTED =
"ACTION_GATT_CONNECTED";
public static final String ACTION_GATT_DISCONNECTED =
"ACTION_GATT_DISCONNECTED";
protected static final String ACTION_GATT_NOTIFICATION_INEXISTENCE = null;
public static final String ACTION_GATT_SERVICES_DISCOVERED =
"ACTION_GATT_SERVICES_DISCOVERED";
public static final String EXTRA_DATA =
"EXTRA_DATA";
Configuration UUIDs: service, read and write, descriptor and heart rate. Where are these things from? Check your beacon manual. If you don't have it, ask the seller or manufacturer to give it to you. You cannot configure anything without the manual.
public static final UUID SERVICE =
UUID.fromString("00001803-494c-4f47-4943-544543480000");
public static final UUID RECEIVE_DATA_CHAR =
UUID.fromString("00001804-494c-4f47-4943-544543480000");
public static final UUID SEND_DATA_CHAR =
UUID.fromString("00001805-494c-4f47-4943-544543480000");
public static final UUID UUID_DESCRIPTOR =
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
public static final UUID UUID_HEART_RATE_MEASUREMENT =
UUID.fromString("00002937-0000-1000-8000-00805f9b34fb");
public static final UUID UUID_CLIENT_CHARACTERISTIC_CONFIG =
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
Next, device and Gatt.
//Device
private BluetoothAdapter mBluetoothAdapter;
private String mBluetoothDeviceAddress;
private BluetoothManager mBluetoothManager;
//Gatt
protected BluetoothGatt mBluetoothGatt;
private final BluetoothGattCallback mGattCallback = new MyGattCallback(HomanBleService.this);
protected BluetoothGattService mGattService;
Initial the service by turn on the Bluetooth.
public boolean initialize() {
//turn the bluetooth on
if (mBluetoothManager == null) {
mBluetoothManager = (BluetoothManager) getSystemService(
Context.BLUETOOTH_SERVICE);
if (mBluetoothManager == null) {
Log.e(TAG, "Unable to initialize BluetoothManager.");
return false;
}
}
mBluetoothAdapter = this.mBluetoothManager.getAdapter();
if (mBluetoothAdapter != null) {
return true;
}
Log.e(TAG, "Unable to obtain a BluetoothAdapter.");
return false;
}
The logcat shortcut.
/* Log tag and shortcut */
final static String TAG = "MYLOG Service";
public static void ltag(String message) { Log.i(TAG, message); }
Next, 1st step: bind and unbind.
//bind service
public class LocalBinder extends Binder {
HomanBleService getService() {
return HomanBleService.this;
}
}
private final IBinder mBinder = new LocalBinder();
public IBinder onBind(Intent intent) { return this.mBinder; }
public boolean onUnbind(Intent intent) {
close();
return super.onUnbind(intent);
}
public void close() {
if (mBluetoothGatt != null) {
mBluetoothGatt.close();
mBluetoothGatt = null;
}
}
You can copy from somewhere else. Most of them are the same code.
2nd, connect and disconnect.
private int mConnectionState = STATE_DISCONNECTED; //status
public int getmConnectionState() { return mConnectionState; }
public void setmConnectionState(int mConnectionState) {
this.mConnectionState = mConnectionState; }
public boolean connect(String address) {
if (mBluetoothAdapter == null || address == null) {
Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
return false;
} else if (mBluetoothDeviceAddress == null ||
!address.equals(mBluetoothDeviceAddress) ||
mBluetoothGatt == null) {
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
//address incorect or too far to reach
if (device == null) {
Log.w(TAG, "Device not found. Unable to connect.");
return false;
}
mBluetoothGatt = device.connectGatt(this, false, this.mGattCallback);
Log.d(TAG, "Trying to create a new connection.");
mBluetoothDeviceAddress = address;
mConnectionState = STATE_CONNECTING;
return true;
} else {
Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection.");
if (!mBluetoothGatt.connect()) { return false; }
mConnectionState = STATE_CONNECTING;
return true;
}
}
public void disconnect() {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter not initialized");
} else {
mBluetoothGatt.disconnect();
}
}
Next, callback and broadcast update. Gatt callback has a lot of lines. Let's separate it into another class, MyGattCallback.java. It contains some tasks. First, we always check the connection change. (SB: Why? Answer: This is wireless connection, man. Too far, we will lost the signal.) Second, check services = service discovered. Third, deal with READ, WRITE and CHANGE callbacks. Are we clear? It not necessary to write them all.
Let's initial some variables for the callback.
class MyGattCallback extends BluetoothGattCallback {
private static final int STATE_CONNECTED = 2;
private static final int STATE_CONNECTING = 1;
private static final int STATE_DISCONNECTED = 0;
private HomanBleService service;
public MyGattCallback(HomanBleService service) {
this.service = service;
}
/* Log tag and shortcut */final static String TAG = "MYLOG GattCB";
public void ltag(String message) { Log.i(TAG, message); }
The connect state change is two ways paths: connect and disconnect.
public void onConnectionStateChange(BluetoothGatt gatt, int status, int state) {
String action;
if (state == STATE_CONNECTED) { //connected //oh, yeah!
action = ACTION_GATT_CONNECTED;
service.setmConnectionState(STATE_CONNECTED);
service.broadcastUpdate(action);
ltag("Connected to GATT server.");
ltag("Start service discovery:"+service.mBluetoothGatt.discoverServices());
} else if (state == STATE_DISCONNECTED) { //disconnected
action = HomanBleService.ACTION_GATT_DISCONNECTED;
service.setmConnectionState(STATE_DISCONNECTED);
ltag("Disconnected from GATT server.");
service.broadcastUpdate(action);
}
} //GattCb -> broadcasUpdate
When the Gatt said connected, it will broadcast back to the phone by broadcastUpdate( action ). It's the same thing with disconnected.
Next, the service discovery:
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
//Gatt: sure, Mr. phone! You can go deeper.
service.broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
//Let's print the list for curiosity
List<BluetoothGattService> list = gatt.getServices();
for (BluetoothGattService bleGattService : list) {
ltag("Service: "+bleGattService.getUuid());
for (BluetoothGattCharacteristic characteristic :
bleGattService.getCharacteristics()) {
ltag(" characteristic: "+characteristic);
for (BluetoothGattDescriptor descriptor :
characteristic.getDescriptors()) {
ltag(" descriptor: "+descriptor.getUuid()+
" , value: "+ Arrays.toString(descriptor.getValue()));
}
}
}
//Get your service by SERVICE UUID
service.mGattService = gatt.getService(SERVICE);
if (service.mGattService != null) {
//Get data with read pass = receive data characteristic
enableNotification(service.mGattService.
getCharacteristic(RECEIVE_DATA_CHAR));
return;
}
//Without a service pass, nothing you can do
service.broadcastUpdate(ACTION_GATT_NOTIFICATION_INEXISTENCE);
service.disconnect();
return;
} else {
ltag("onServicesDiscovered received: " + status);
}
} //call broadcastUpdate or disconnect
//set permission
public boolean enableNotification(BluetoothGattCharacteristic characteristic) {
ltag("Enable Notification ->" + characteristic.getUuid());
ltag("Set Characteristic Notification =>"+
service.mBluetoothGatt.setCharacteristicNotification(characteristic, true));
//get data with descriptor pass
BluetoothGattDescriptor clientConfig =
characteristic.getDescriptor(UUID_DESCRIPTOR);
if (clientConfig == null) {
//Check your manual for descriptor UUID
Log.e(TAG, "Client Config is null. No permission");
return false; //No, you can't do anything
}
clientConfig.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
return service.mBluetoothGatt.writeDescriptor(clientConfig);
}
Here is your egg. If you want to get to the core, you need three UUIDs as your keys. Once you crack the safe, you can have your crown jewel. (Homan: Tasty?)
Next, read, write and change:
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
service.broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
ltag("Read is successful.");
}
}
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
service.broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
ltag("Write is successful.");
}
}
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
//you can have next business, one thing at a time.
service.broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
That's all about callback. If you are confused, you can go to cook a egg. Or you can send me an egg, I want a gold one. Let's more back to service code.
Beside the callback, you need a sender to broadcast the signal. It's handled by broadcastUpdate().
//from GattCb call -- ConnectionStateChange
protected void broadcastUpdate(String action) {
sendBroadcast(new Intent(action)); //signal
}
//from GattCb call -- ServicesDiscovered, Read, Write and Change
protected void broadcastUpdate(String action,
BluetoothGattCharacteristic characteristic) {
Intent intent = new Intent(action);
//calibrate the UUID
if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
int format;
if ((characteristic.getProperties() & 1) != 0) {
format = 18;
Log.d(TAG, "Heart rate format UINT16.");
} else {
format = 17;
Log.d(TAG, "Heart rate format UINT8.");
}
// type offset
int heartRate = characteristic.getIntValue(format, 1).intValue();
Log.d(TAG, String.format("Received heart rate: %d",
new Object[]{Integer.valueOf(heartRate)}));
//send back heartRate
intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));
//other tasks
} else {
byte[] data = characteristic.getValue();
if (data != null && data.length > 0) {
StringBuilder stringBuilder = new StringBuilder(data.length);
int length = data.length;
for (int i = 0; i < length; i++) {
stringBuilder.append(String.format("%02X ",
new Object[]{ Byte.valueOf(data[i]) }));
}
intent.putExtra(EXTRA_DATA, new StringBuilder(String.valueOf(new String(data)))
.append("\n").append(stringBuilder.toString()).toString());
}
}
sendBroadcast(intent); //signal
}
You can treat it like a wireless intent. It indeed uses Intent. To change your UUID, 8 bit and 16 bit are one bit different. You need to get it right before you send the data. In other tasks, the sender only accepts byte code.
This pair is done. Let's move on Read and Write in service. Do you like my egg? We need to ask Gatt to do the same thing, notification, read and write.
First, Read:
public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter not initialized");
} else {
mBluetoothGatt.readCharacteristic(characteristic);
}
}
Second, Write:
public boolean writeData(byte[] data, UUID service, UUID charac) {
BluetoothGattService mainService = mBluetoothGatt.getService(service);
if (mainService == null) {
Log.e(TAG, "service not found!");
return false;
}
BluetoothGattCharacteristic writeCharac =
mainService.getCharacteristic(charac);
if (writeCharac == null) {
Log.e(TAG, "HEART RATE MEASUREMENT charateristic not found!");
return false;
}
writeCharac.setValue(data);
return mBluetoothGatt.writeCharacteristic(writeCharac);
}
Two passes you need to check, service and characteristic.
Third, Notification:
public void setCharacteristicNotification( BluetoothGattCharacteristic characteristic, boolean enabled) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter not initialized");
return;
}
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
//permission
BluetoothGattDescriptor descriptor = characteristic
.getDescriptor(UUID_CLIENT_CHARACTERISTIC_CONFIG);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
}
}
Descriptor is your inner core, your last defense.
The Optional part, if someone want to print the service, you can give them a list.
public List<BluetoothGattService> getSupportedGattServices() {
if (mBluetoothGatt == null) {
return null;
}
return mBluetoothGatt.getServices();
}
Finally, we need to test our service in DeviceControlActivty. Please move to next part.
My AD: If anyone likes my work, please send me an email, message, call or gift card (I am out of coffee). I am out of job market right now. I will be glad to help with any projects, including your homework.
HomanHuang@gmail.com, 415-286-6020