UART GATT Server (Peripheral) on Android 模擬 Peripheral (like BT SPP)

from: https://thejeshgn.com/2016/12/11/uart-gatt-server-peripheral-on-android/

code: https://github.com/fwengineer/BleUARTPeripheral
apk: https://github.com/fwengineer/BleUARTPeripheral/raw/master/app/app-release.apk

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 mobilesdesktops. 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.

The UART over BLE Protocol flow.

The UART over BLE Protocol flow.

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.

Simple UART GATT Server. Showing the clients connected to it.

Simple UART GATT Server. Showing the clients connected to it.

UART GATT Client App. Talking to Server.

UART GATT Client App. Talking to Server.

未經允許不得轉載:GoMCU » UART GATT Server (Peripheral) on Android 模擬 Peripheral (like BT SPP)