I demonstrate how to write a simple BLE peripheral application in Android here. I am bad in Android development, The UI would be very ugly, but the code work:
Currently(5/25/2015),
the code could be running in Nexus 6 or Nexus 9 only based on my test. The other phones or tablets DO NOT support to be a BLE peripheral. So, if you really interested in the issue of Android as BLE Peripheral , please open your wallet or swipe your card, to buy a GOOGLE official device , thank you.
To add a characteristic as notification is little bit complicated, In here I just add read and write characteristics.
About the notification, I put code in the last part of this post.
You should add
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
The 2 lines in your AndroidManifest.xml, like this :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gaiger.simplebleperipheral"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="21" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
The kernal code are below , note the AdvertiseCallback is callback of BluetoothLeAdvertiser ::startAdvertising, and BluetoothGattServerCallback is callback function of ALL BluetoothGattCharacteristic.
BLEPeripheral.java: (that is what you want)
package com.gaiger.simplebleperipheral;
import java.util.List;
import java.util.UUID;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.util.Log;
public class BLEPeripheral{
public interface ConnectionCallback {
void onConnectionStateChange(BluetoothDevice device, int newState);
}
BluetoothManager mManager;
BluetoothAdapter mAdapter;
BluetoothLeAdvertiser mLeAdvertiser;
AdvertiseSettings.Builder settingBuilder;
AdvertiseData.Builder advBuilder;
BluetoothGattServer mGattServer;
ConnectionCallback mConnectionCallback;
public interface WriteCallback {
void onWrite(byte[] data);
}
WriteCallback mWriteCallback;
public static boolean isEnableBluetooth(){
return BluetoothAdapter.getDefaultAdapter().isEnabled();
}
public int init(Context context){
if(null == mManager)
{
mManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
if(null == mManager)
return -1;
if(false == context.getPackageManager().
hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE))
return -2;
}
if(null == mAdapter)
{
mAdapter = mManager.getAdapter();
if(false == mAdapter.isMultipleAdvertisementSupported())
return -3;
}
if(null == settingBuilder)
{
settingBuilder = new AdvertiseSettings.Builder();
settingBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY);
settingBuilder.setConnectable(true);
settingBuilder.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH);
}
if(null == advBuilder)
{
advBuilder = new AdvertiseData.Builder();
mAdapter.setName("SimplePeripheral");
advBuilder.setIncludeDeviceName(true);
}
if(null == mGattServer)
{
mGattServer = mManager.openGattServer(context, mGattServerCallback);
if(null == mGattServer)
return -4;
addDeviceInfoService();
}
return 0;
}
public void setConnectionCallback(ConnectionCallback callback)
{
mConnectionCallback = callback;
}
public void close()
{
if(null != mLeAdvertiser)
stopAdvertise();
if(null != mGattServer)
mGattServer.close();
mGattServer = null;
if(null != advBuilder)
advBuilder = null;
if(null != settingBuilder)
settingBuilder = null;
if(null != mAdapter)
mAdapter = null;
if(null != mManager)
mManager = null;
}
public static String getAddress(){return BluetoothAdapter.getDefaultAdapter().getAddress();}
private AdvertiseCallback mAdvCallback = new AdvertiseCallback() {
@Override
public void onStartFailure(int errorCode){
Log.d("advertise","onStartFailure");
}
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect){
Log.d("advertise","onStartSuccess");
};
};
private final BluetoothGattServerCallback mGattServerCallback
= new BluetoothGattServerCallback(){
@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState){
Log.d("GattServer", "Our gatt server connection state changed, new state ");
Log.d("GattServer", Integer.toString(newState));
if(null != mConnectionCallback && BluetoothGatt.GATT_SUCCESS == status)
mConnectionCallback.onConnectionStateChange(device, newState);
super.onConnectionStateChange(device, status, newState);
}
@Override
public void onServiceAdded(int status, BluetoothGattService service) {
Log.d("GattServer", "Our gatt server service was added.");
super.onServiceAdded(status, service);
}
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
Log.d("GattServer", "Our gatt characteristic was read.");
super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
characteristic.getValue());
}
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
Log.d("GattServer", "We have received a write request for one of our hosted characteristics");
//Log.d("GattServer", "data = "+ value.toString());
super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value);
if(null != mWriteCallback)
mWriteCallback.onWrite(value);
}
@Override
public void onNotificationSent(BluetoothDevice device, int status)
{
Log.d("GattServer", "onNotificationSent");
super.onNotificationSent(device, status);
}
@Override
public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
Log.d("GattServer", "Our gatt server descriptor was read.");
super.onDescriptorReadRequest(device, requestId, offset, descriptor);
}
@Override
public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
Log.d("GattServer", "Our gatt server descriptor was written.");
super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value);
}
@Override
public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
Log.d("GattServer", "Our gatt server on execute write.");
super.onExecuteWrite(device, requestId, execute);
}
};
private void addDeviceInfoService()
{
if(null == mGattServer)
return;
final String SERVICE_DEVICE_INFORMATION = "0000180a-0000-1000-8000-00805f9b34fb";
final String SOFTWARE_REVISION_STRING = "00002A28-0000-1000-8000-00805f9b34fb";
BluetoothGattService previousService =
mGattServer.getService( UUID.fromString(SERVICE_DEVICE_INFORMATION));
if(null != previousService)
mGattServer.removeService(previousService);
BluetoothGattCharacteristic softwareVerCharacteristic = new BluetoothGattCharacteristic(
UUID.fromString(SOFTWARE_REVISION_STRING),
BluetoothGattCharacteristic.PROPERTY_READ,
BluetoothGattCharacteristic.PERMISSION_READ
);
BluetoothGattService deviceInfoService = new BluetoothGattService(
UUID.fromString(SERVICE_DEVICE_INFORMATION),
BluetoothGattService.SERVICE_TYPE_PRIMARY);
softwareVerCharacteristic.setValue(new String("0.0.0").getBytes());
deviceInfoService.addCharacteristic(softwareVerCharacteristic);
mGattServer.addService(deviceInfoService);
}
public void setService(String read1Data, String read2Data, WriteCallback
writeCallBack)
{
if(null == mGattServer)
return ;
stopAdvertise();
final String SERVICE_A = "0000fff0-0000-1000-8000-00805f9b34fb";
final String CHAR_READ1 = "0000fff1-0000-1000-8000-00805f9b34fb";
final String CHAR_READ2 = "0000fff2-0000-1000-8000-00805f9b34fb";
final String CHAR_WRITE = "0000fff3-0000-1000-8000-00805f9b34fb";
final String CHAR_NOTIFY = "0000fff4-0000-1000-8000-00805f9b34fb";
BluetoothGattService previousService =
mGattServer.getService( UUID.fromString(SERVICE_A));
if(null != previousService)
mGattServer.removeService(previousService);
BluetoothGattCharacteristic read1Characteristic = new BluetoothGattCharacteristic(
UUID.fromString(CHAR_READ1),
BluetoothGattCharacteristic.PROPERTY_READ,
BluetoothGattCharacteristic.PERMISSION_READ
);
BluetoothGattCharacteristic read2Characteristic = new BluetoothGattCharacteristic(
UUID.fromString(CHAR_READ2),
BluetoothGattCharacteristic.PROPERTY_READ,
BluetoothGattCharacteristic.PERMISSION_READ
);
BluetoothGattCharacteristic writeCharacteristic = new BluetoothGattCharacteristic(
UUID.fromString(CHAR_WRITE),
BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_WRITE
);
read1Characteristic.setValue(read1Data.getBytes());
read2Characteristic.setValue(read2Data.getBytes());
mWriteCallback = writeCallBack;
BluetoothGattService AService = new BluetoothGattService(
UUID.fromString(SERVICE_A),
BluetoothGattService.SERVICE_TYPE_PRIMARY);
AService.addCharacteristic(read1Characteristic);
AService.addCharacteristic(read2Characteristic);
AService.addCharacteristic(writeCharacteristic);
final BluetoothGattCharacteristic notifyCharacteristic = new BluetoothGattCharacteristic(
UUID.fromString(CHAR_NOTIFY),
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ
);
notifyCharacteristic.setValue(new String("0"));
AService.addCharacteristic(notifyCharacteristic);
final Handler handler = new Handler();
Thread thread = new Thread() {
int i = 0;
@Override
public void run() {
while(true) {
try {
sleep(1500);
} catch (InterruptedException e) {}
handler.post(this);
List<BluetoothDevice> connectedDevices
= mManager.getConnectedDevices(BluetoothProfile.GATT);
if(null != connectedDevices)
{
notifyCharacteristic.setValue(String.valueOf(i).getBytes());
if(0 != connectedDevices.size())
mGattServer.notifyCharacteristicChanged(connectedDevices.get(0),
notifyCharacteristic, false);
}
i++;
}
}
};
thread.start();
mGattServer.addService(AService);
}
public void startAdvertise(String scanRespenseName)
{
mAdapter.setName(scanRespenseName);
advBuilder.setIncludeDeviceName(true);
startAdvertise();
}
public void startAdvertise()
{
if(null == mAdapter)
return;
if (null == mLeAdvertiser)
mLeAdvertiser = mAdapter.getBluetoothLeAdvertiser();
if(null == mLeAdvertiser)
return;
mLeAdvertiser.startAdvertising(settingBuilder.build(),
advBuilder.build(), mAdvCallback);
}
public void stopAdvertise()
{
if(null != mLeAdvertiser)
mLeAdvertiser.stopAdvertising(mAdvCallback);
mLeAdvertiser = null;
}
}
There is a callback interface WriteCallback to hold tthe data which the BLE has written.
MainActivity.java : (UI part)
package com.gaiger.simplebleperipheral;
import java.io.UnsupportedEncodingException;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity {
private BLEPeripheral blePeri;
private CheckBox adverstiseCheckBox;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
adverstiseCheckBox = (CheckBox) findViewById(R.id.advertise_checkBox);
blePeri = new BLEPeripheral();
adverstiseCheckBox.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(true == adverstiseCheckBox.isChecked())
{
TextView textView;
textView = (TextView)findViewById(R.id.status_textView);
textView.setText("advertising");
blePeri.setService(new String("GAIGER"),
new String("AndroidBLE"),
mWrittenCallback
);
blePeri.startAdvertise();
}
else
{
TextView textView;
textView = (TextView)findViewById(R.id.status_text);
textView.setText("disable");
blePeri.stopAdvertise();
}
}
});
adverstiseCheckBox.setEnabled(false);
if(false == BLEPeripheral.isEnableBluetooth())
{
Intent intentBtEnabled = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
// The REQUEST_ENABLE_BT constant passed to startActivityForResult() is a locally defined integer (which must be greater than 0), that the system passes back to you in your onActivityResult()
// implementation as the requestCode parameter.
int REQUEST_ENABLE_BT = 1;
startActivityForResult(intentBtEnabled, REQUEST_ENABLE_BT);
Toast.makeText(this, "Please enable bluetooth and execute the application agagin.",
Toast.LENGTH_LONG).show();
}
}
byte[] writtenByte;
BLEPeripheral.WriteCallback mWrittenCallback = new BLEPeripheral.WriteCallback()
{
@Override
public
void onWrite(byte[] data)
{
writtenByte = data.clone();
Thread timer = new Thread(){
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
TextView textView;
textView = (TextView)findViewById(R.id.written_textView);
try {
textView.setText(new String(writtenByte, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
};
timer.start();
}
};
Runnable mCleanTextRunnable = new Runnable() {
public void run() {
TextView textView;
textView = (TextView)findViewById(R.id.connected_textView);
textView.setText("no connection");
}
};
Handler mConnectTextHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
String data = (String)msg.obj;
switch (msg.what) {
case 0:
data += new String(" disconnected");
this.postDelayed(mCleanTextRunnable, 3000);
break;
case 2:
data += new String(" connected");
break;
default:
break;
}
TextView textView;
textView = (TextView)findViewById(R.id.connected_textView);
textView.setText(data);
}
};
@Override
public void onResume(){
super.onResume();
int sts;
sts = blePeri.init(this);
// blePeri.mConnectionCallback = new BLEPeripheral.ConnectionCallback (){
// @Override
// public void onConnectionStateChange(BluetoothDevice device, int newState){
// Log.d("main","onConnectionStateChange");
// }
// };
//
blePeri.setConnectionCallback( new BLEPeripheral.ConnectionCallback (){
@Override
public void onConnectionStateChange(BluetoothDevice device, int newState){
Message msg = new Message();
msg.what = newState;
msg.obj = new String( device.getName() +" "+ device.getAddress() );
mConnectTextHandler.sendMessage(msg);
}
}
);
if(0 > sts)
{
if(-1 == sts)
Toast.makeText(this, "this device is without bluetooth module",
Toast.LENGTH_LONG).show();
if(-2 == sts)
Toast.makeText(this, "this device do not support Bluetooth low energy",
Toast.LENGTH_LONG).show();
if(-3 == sts)
Toast.makeText(this, "this device do not support to be a BLE peripheral, " +
"please buy nexus 6 or 9 then try again",
Toast.LENGTH_LONG).show();
finish();
}
TextView textView;
textView = (TextView)findViewById(R.id.mac_textView);
textView.setText(BLEPeripheral.getAddress());
adverstiseCheckBox.setEnabled(true);
}
@Override
protected void onStop() {
super.onStop();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
activity_main.xml: (layout, very ugly)
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.awind.presentsenseperipheral.MainActivity" >
<TextView
android:id="@+id/mac_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/status_text"
android:layout_marginLeft="18dp"
android:layout_toRightOf="@+id/status_text"
android:text="00:11:22:AA:BB:CC"
android:textAppearance="?android:attr/textAppearanceLarge" />
<CheckBox
android:id="@+id/advertise_checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/mac_text"
android:layout_marginLeft="34dp"
android:layout_marginTop="41dp"
android:text="Advertise" />
<TextView
android:id="@+id/status_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/advertise_checkBox"
android:layout_alignParentTop="true"
android:layout_marginTop="124dp"
android:text="Disable"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/connected_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/mac_text"
android:layout_centerVertical="true"
android:text="no connection"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/written_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/mac_text"
android:layout_alignParentBottom="true"
android:text="WrittenText"
android:textAppearance="?android:attr/textAppearanceLarge" />
</RelativeLayout>
I do not like to write too much explanation in here, One said: if you could implement, you understand it; if you could not, you know about nothing of it .
Notice the part of notifyCharacteristic ,I create a thread, which updates value and send a signal to BluetoothGattServer, to inform the BLE central the value has changed.
There is a bug in written text update: once WriteCallback::onWrite been called, the runOnUiThread for updating R.id.written_textView should be executed once. But in my code, the update would not work. That is very minior for the purpose of demonstration BLE on Android, So, please ignore it.