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 not support to be a BLE peripheral. So, if you really interested in the Android as peripheral issue, please open your wallet and buy a GOOGLE official device, thank you.
How to add a characteristic as notification is little bit complicated, In here I just add read write ones.
About the notification, I put code in the lat 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.awind.presentsenseperipheral" 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.awind.presentsenseperipheral;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.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.util.Log;public class BLEPeripheral{ Context mContext; BluetoothManager mManager; BluetoothAdapter mAdapter; BluetoothLeAdvertiser mLeAdvertiser; BluetoothGattServer mGattServer; 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; mContext = context; return 0; } public void close() { } 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)); 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); } @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(BluetoothGattServer gattServer) { if(null == gattServer) return; // // device info // final String SERVICE_DEVICE_INFORMATION = "0000180a-0000-1000-8000-00805f9b34fb"; final String SOFTWARE_REVISION_STRING = "00002A28-0000-1000-8000-00805f9b34fb"; 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.1").getBytes()); deviceInfoService.addCharacteristic(softwareVerCharacteristic); gattServer.addService(deviceInfoService); } public void startAdvertise() { if(null == mAdapter) return; if (null == mLeAdvertiser) mLeAdvertiser = mAdapter.getBluetoothLeAdvertiser(); if(null == mLeAdvertiser) return; AdvertiseSettings.Builder settingBuilder; settingBuilder = new AdvertiseSettings.Builder(); settingBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY); settingBuilder.setConnectable(true); settingBuilder.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH); AdvertiseData.Builder advBuilder; advBuilder = new AdvertiseData.Builder(); mAdapter.setName("PeripheralAndroid"); //8 characters works, 9+ fails advBuilder.setIncludeDeviceName(true); mGattServer = mManager.openGattServer(mContext, mGattServerCallback); addDeviceInfoService(mGattServer); final String SERVICE_A = "0000fff0-0000-1000-8000-00805f9b34fb"; final String CHAR_READ_1 = "00fff1-0000-1000-8000-00805f9b34fb"; final String CHAR_READ_2 = "00fff2-0000-1000-8000-00805f9b34fb"; final String CHAR_WRITE = "00fff3-0000-1000-8000-00805f9b34fb"; BluetoothGattCharacteristic read1Characteristic = new BluetoothGattCharacteristic( UUID.fromString(CHAR_READ_1), BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ ); read1Characteristic.setValue(new String("this is read 1").getBytes()); BluetoothGattCharacteristic read2Characteristic = new BluetoothGattCharacteristic( UUID.fromString(CHAR_READ_2), BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ ); read2Characteristic.setValue(new String("this is read 2").getBytes()); BluetoothGattCharacteristic writeCharacteristic = new BluetoothGattCharacteristic( UUID.fromString(CHAR_WRITE), BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE ); / BluetoothGattService AService = new BluetoothGattService( UUID.fromString(SERVICE_A), BluetoothGattService.SERVICE_TYPE_PRIMARY); AService.addCharacteristic(read1Characteristic); AService.addCharacteristic(read2Characteristic); AService.addCharacteristic(writeCharacteristic); // Add notify characteristic here !!! mGattServer.addService(AService); mLeAdvertiser.startAdvertising(settingBuilder.build(), advBuilder.build(), mAdvCallback); } public void stopAdvertise() { if(null != mLeAdvertiser) mLeAdvertiser.stopAdvertising(mAdvCallback); mLeAdvertiser = null; }}
MainActivity.java : (UI part)
package com.awind.presentsenseperipheral;import android.app.Activity;import android.bluetooth.BluetoothAdapter;import android.content.Intent;import android.os.Bundle;import android.view.Menu;import android.view.MenuItem;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_text); textView.setText("advertising"); 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(); } } @Override public void onResume(){ super.onResume(); int sts; sts = blePeri.init(this); 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_text); 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/status_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_marginLeft="15dp" android:layout_marginTop="25dp" android:text="Disable" android:textAppearance="?android:attr/textAppearanceMedium" /> <TextView android:id="@+id/mac_text" 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_alignLeft="@+id/status_text" android:layout_below="@+id/mac_text" android:layout_marginTop="34dp" android:text="Advertise" /></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 .
About notification characteristic:
Replace the line :
// Add nodify characteristic here !!!
final String CHAR_NOTIFY = "00fffB-0000-1000-8000-00805f9b34fb"; final BluetoothGattCharacteristic notifyCharacteristic = new BluetoothGattCharacteristic( UUID.fromString(CHAR_NOTIFY), BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ ); notifyCharacteristic.setValue(new String("0")); presentSenseService.addCharacteristic(notifyCharacteristic); final Handler handler = new Handler(); Thread thread = new Thread() { int i = 0; @Override public void run() { try { while(true) { sleep(1500); handler.post(this); List<BluetoothDevice> connectedDevices = mManager.getConnectedDevices(BluetoothProfile.GATT); if(null != connectedDevices) { notifyCharacteristic.setValue(String.valueOf(i).getBytes()); mGattServer.notifyCharacteristicChanged(connectedDevices.get(0), notifyCharacteristic, false); } i++; } } catch (InterruptedException e) { e.printStackTrace(); } } }; thread.start();
That is, create a thread , that updates value and send a signal to BluetoothGattServer to note the value has been change.