from: https://thejeshgn.com/2016/12/11/uart-gatt-server-peripheral-on-android/
In most BLE scenarios, Android app is a client (GATT Client). But one can also use Android as a GATT Server. There are use-cases where running a GATT Server on Android can be useful. For example let’s say you want a desktop app to display SMS notifications. It’s easy to write a GATT server (on Phone) that pushes the message to Client (Desktop) as and when SMS arrives.
UART is the most popular protocol used for talking to a computer device over serial port. If we implement a UART GATT server, it should solve our problem. What I am talking here is not exactly UART in traditional sense. It’s an emulation of serial port over BLE. Its one of the best ways for implementing Android to Android or Android to Desktop communication over a simple protocol.
There are many UART client apps and libraries for mobiles, desktops. UART GATT Servers are usually written for sensors, tags etc. So in this how-to I am implementing an Android app – A GATT server that talks UART over BLE. You could use it for sending SMS alerts or do any other communication.
In the example code below I am writing a simple server, once connected it responds to commands like whoami or date etc. If it can’t figure then it just echoes whatever client has sent. The diagram above should explain the flow. Lets jump into code part.
The SDK version should be at least 21 and we need Bluetooth admin permissions. So enable them in AndroidManifest.xml
1 2 3 4 5 6 | < uses-sdk android:minSdkVersion = "21" android:targetSdkVersion = "21" /> < uses-permission android:name = "android.permission.BLUETOOTH" /> < uses-permission android:name = "android.permission.BLUETOOTH_ADMIN" /> |
Define the UUIDs for the service and characteristics. I have written about them previously. You can get more info there. All the permissions are from client perspective.
Important to note that we need Client Characteristic Configuration 0x2902 defined on GATT Server so clients can enable and receive notification. We have defined them in a class called UARTProfile.java
1 2 3 4 5 6 7 8 9 | //Part of UARTProfile.java //Service UUID to expose our UART characteristics public static UUID UART_SERVICE = UUID.fromString( "6e400001-b5a3-f393-e0a9-e50e24dcca9e" ); //RX, Write characteristic public static UUID RX_WRITE_CHAR = UUID.fromString( "6e400002-b5a3-f393-e0a9-e50e24dcca9e" ); //TX Read Notify public static UUID TX_READ_CHAR = UUID.fromString( "6e400003-b5a3-f393-e0a9-e50e24dcca9e" ); public static UUID TX_READ_CHAR_DESC = UUID.fromString( "00002902-0000-1000-8000-00805f9b34fb" ); public final static int DESCRIPTOR_PERMISSION = BluetoothGattDescriptor.PERMISSION_WRITE; |
GATT Servers work similar to any other service providing API servers. You define services with permissions and expose them. Clients connect to server, explore the services. Write to a characteristic, read from a characteristic or get notified. In our example all of it happens in a MainActivity. I have bits of code here to explain the process.
First we need to get an instance of mBluetoothManager so we create a GattServer. mGattServerCallback is an instance of BluetoothGattServerCallback which handles the request from clients to this server. We will look into it later. After that we will initialize the server with services and start advertising so clients can find.
1 2 3 4 5 6 7 8 9 | //.... mBluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE); mBluetoothAdapter = mBluetoothManager.getAdapter(); //... mGattServer = mBluetoothManager.openGattServer( this , mGattServerCallback); //... initServer(); startAdvertising(); //... |
We need to define the services and characteristics that we are going to provide as part of our GATTServer. This is done a part of initServer method in our case. Start with defining services and Characteristics. Make sure property types and permissions are right and the add them to the GattServer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | private void initServer() { BluetoothGattService UART_SERVICE = new BluetoothGattService(UARTProfile.UART_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY); BluetoothGattCharacteristic TX_READ_CHAR = new BluetoothGattCharacteristic(UARTProfile.TX_READ_CHAR, //Read-only characteristic, supports notifications BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ); //Descriptor for read notifications BluetoothGattDescriptor TX_READ_CHAR_DESC = new BluetoothGattDescriptor(UARTProfile.TX_READ_CHAR_DESC, UARTProfile.DESCRIPTOR_PERMISSION); TX_READ_CHAR.addDescriptor(TX_READ_CHAR_DESC); BluetoothGattCharacteristic RX_WRITE_CHAR = new BluetoothGattCharacteristic(UARTProfile.RX_WRITE_CHAR, //write permissions BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE); UART_SERVICE.addCharacteristic(TX_READ_CHAR); UART_SERVICE.addCharacteristic(RX_WRITE_CHAR); mGattServer.addService(UART_SERVICE); } |
Once the services are added, we need to advertise them so clients can explore and make use of them. Create AdvertiseSettings (you can experiment with the values, I have used what works for me). The add the Services you want to advertise and start advertising.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | private void startAdvertising() { if (mBluetoothLeAdvertiser == null ) return ; AdvertiseSettings settings = new AdvertiseSettings.Builder() .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED) .setConnectable( true ) .setTimeout( 0 ) .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) .build(); AdvertiseData data = new AdvertiseData.Builder() .setIncludeDeviceName( true ) .addServiceUuid( new ParcelUuid(UARTProfile.UART_SERVICE)) .build(); mBluetoothLeAdvertiser.startAdvertising(settings, data, mAdvertiseCallback); } |
As you know BluetoothGattServerCallback is the one handles all the connections from clients and serves them as per need. There are many methods that we override to provide our own implementation. Below I have snippets for some important ones.
onConnectionStateChange is the one which gets called when the connection state with a client changes. We can handle them to keep a list of clients or initialize things when a client connects etc.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() { //.... @Override public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { super .onConnectionStateChange(device, status, newState); Log.i(TAG, "onConnectionStateChange " +UARTProfile.getStatusDescription(status)+ " " +UARTProfile.getStateDescription(newState)); if (newState == BluetoothProfile.STATE_CONNECTED) { postDeviceChange(device, true ); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { postDeviceChange(device, false ); } } .... // |
onCharacteristicWriteRequest is the method that gets called when a client writes to any characteristic on GattServer. Hence we need to check characteristic.getUuid() to find on which characteristic the client has operated on and respond accordingly. Here once I get the message, I send a success response and then handle our reply using method sendOurResponse.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | //... @Override public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte [] value) { super .onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value); Log.i(TAG, "onCharacteristicWriteRequest " + characteristic.getUuid().toString()); if (UARTProfile.RX_WRITE_CHAR.equals(characteristic.getUuid())) { //IMP: Copy the received value to storage storage = value; if (responseNeeded) { mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0 , value); Log.d(TAG, "Received data on " + characteristic.getUuid().toString()); Log.d(TAG, "Received data" + bytesToHex(value)); } //IMP: Respond sendOurResponse(); mHandler.post( new Runnable() { @Override public void run() { Toast.makeText(MainActivity. this , "We received data" , Toast.LENGTH_SHORT).show(); } }); } } //.... |
Since our responses are notifications. Clients need to set the Descriptor (UUID 0x2902) to get the response notifications. Hence on the server side we need to provide methods to read and write Descriptors. Remember descriptor write needs to have a response sent back with GATT_SUCCESS. So client knows. Else client assumes something has gone wrong and disconnects, usually throwing error 133 or 22. Its hard to debug.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | //...... @Override public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) { Log.d( "HELLO" , "Our gatt server descriptor was read." ); super .onDescriptorReadRequest(device, requestId, offset, descriptor); Log.d( "DONE" , "Our gatt server descriptor was read." ); } @Override public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte [] value) { Log.d( "HELLO" , "Our gatt server descriptor was written." ); super .onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value); Log.d( "DONE" , "Our gatt server descriptor was written." ); //NOTE: Its important to send response. It expects response else it will disconnect if (responseNeeded) { mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0 , value); } } //.... |
Our last method is our own sendOurResponse. We we process the input and send the notification. Please note we use setValue and notifyCharacteristicChanged to send the notification to connected device.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | //Send notification to all the devices once you write private void sendOurResponse() { for (BluetoothDevice device : mConnectedDevices) { BluetoothGattCharacteristic readCharacteristic = mGattServer.getService(UARTProfile.UART_SERVICE) .getCharacteristic(UARTProfile.TX_READ_CHAR); byte [] notify_msg = storage; String hexStorage = bytesToHex(storage); Log.d(TAG, "received string = " + bytesToHex(storage)); if (hexStorage.equals( "77686F616D69" )) { notify_msg = "I am echo an machine" .getBytes(); } else if (bytesToHex(storage).equals( "64617465" )) { DateFormat dateFormat = new SimpleDateFormat( "yyyy/MM/dd HH:mm:ss" ); Date date = new Date(); notify_msg = dateFormat.format(date).getBytes(); } else { //TODO: Do nothing send what you received. Basically echo } readCharacteristic.setValue(notify_msg); Log.d(TAG, "Sending Notifications" + notify_msg); boolean is_notified = mGattServer.notifyCharacteristicChanged(device, readCharacteristic, false ); Log.d(TAG, "Notifications =" + is_notified); } } |
This is not complete, of course you need to better handling of disconnect, shutdown and startup etc But this gives you an idea how easy it is to write GattServer on Android.
You can see all code on GitHub at release v1.0 of BleUARTPeripheral. The release also has prebuilt android app, its also on Playstore. Screenshots of the communication are below featuring NRF UARTApp as the client.