2015年5月25日 星期一

Android as Bluetooth Low Energy Peripheral (GATT server).



  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.

2 則留言:

  1. Hi Chen, In which apart of the code are you setting up 'generic access service' and 'generic attribute service'. To be precise, I want to add 'Peripheral Preferred Connection Parameters' characteristics to the 'Generic Access Service'

    回覆刪除