The original address


The purpose of this article is to provide a simple tutorial for students who are not familiar with Bluetooth Low Energy on Android, so some details are not discussed too deeply. In addition, in order to enable students without Ble devices to simulate the interaction process with the device, This article also provides sample code for central and peripheral devices, where you can happily “fight left and right” with just two phones.

The preparatory work

role

Above we mentioned the central and peripheral, here we can understand it simply as follows:

  • The central equipment (central) : after receiving the radio signal from the peripheral equipment can initiate the connection of main equipment, such as when we worship to the bicycle lock our mobile phones is that bike as the central link and open the lock, and a series of operations, a central device is usually the same below time can only establish connection with up to 7 peripheral devices.
  • Peripheral: a peripheral device that can be connected to a central device. Only one central device can connect to a peripheral at a time.

Note: Android has supported Bluetooth low power since 4.3(API Level 18), but only as central mode at first, and only as peripheral mode since Android 5.0(API Level 21). Therefore, it is best to use Android 5.0 or higher for the following operations.

Required permissions

  <uses-permission android:name="android.permission.BLUETOOTH" />
  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
  // To use BLE scan, you need to go to Settings > Security and Location information > Location Information to open location information.
  // Otherwise, the surrounding devices will not be searched
Copy the code

Why do you need location permission to use Bluetooth low Power? Simply put, Bluetooth also has the function of location.

The sample code

  • Peripheral equipment
  • The central equipment

start

Next, we are ready to start the actual operation. First, we prepare two mobile phones, with mobile phone A as the central device and mobile phone B as the peripheral device. After turning on THE BLE broadcast of mobile phone B, we use mobile phone A to turn on bluetooth –> scan –> connect –> obtain service. Features -> Open notifications -> Write features -> Read features -> Disconnect, through these steps we can learn the use of Android Ble basic methods.

Starting scanning, the next in these operations you may encounter all sorts of strange questions, to reduce the number of people on the pit probability, I’ll share some might encounter in the back of the operation of the problems and solutions, some problems in the official document may have mentioned, some is mentioned in some BBS posts, and some of his own experience.

Open the bluetooth

There are two ways to enable Bluetooth:

    / / method
    BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    BluetoothAdapter mBluetoothAdapter = bluetoothManager.getAdapter();
    if(mBluetoothAdapter ! =null){
      mBluetoothAdapter.enable();
    }
Copy the code
    / / method 2
    BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    BluetoothAdapter mBluetoothAdapter = bluetoothManager.getAdapter();
    if(! mBluetoothAdapter.isEnabled() && ! mBluetoothAdapter.isEnabled()) { Intent enableBtIntent =new Intent(
          BluetoothAdapter.ACTION_REQUEST_ENABLE);
      startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }
Copy the code
  • In method 1, bluetooth will be directly turned on; in method 2, the system Activity will be switched on manually by the user

scanning

Scanning is a very power-intensive operation, so we should stop scanning as soon as we find the device we need. The official provides two scanning methods:

  / / the old API
  // Start scanning
  private void scan(a){
    BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    bluetoothManager.getAdapter().startLeScan(mLeScanCallback);

    // If you want to specify a search device, you can use the following constructor to pass in an array of UUids for the service that the peripheral broadcasts
    UUID[] uuids=new UUID[]{UUID_ADV_SERVER};
    bluetoothManager.getAdapter().startLeScan(uuids,mLeScanCallback);
  }

  // Stop scanning
  private void stopScan(a){
    BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    bluetoothManager.getAdapter().stopLeScan(mLeScanCallback);
  }

  // Scan result callback
  LeScanCallback mLeScanCallback = new LeScanCallback() {
      @Override
      public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
        // Device: Detected Bluetooth device object
        // RSSI: indicates the signal strength of the scanned device. This is a negative value. A higher value indicates a higher signal strength
        //scanRecord: indicates the scanned data broadcast by the device, including the device name and service UUID}};Copy the code

This is an API that was deprecated in Android 5.0 and is still available today. Since the broadcast data of the device called back in onLeScan needs to be manually parsed, this is a rather cumbersome process.

The new API has encapsulated methods to parse the broadcast data. If you want to parse the broadcast data using the old scanning method for adaptation, you can use the parsing method used by the new API in the source code (slightly modified, If you want to learn more about how to parse broadcast data, see Volume 3, Part C, Section 11 in Core Specifications 5.0.

  // New API, requires Android 5.0(API Level 21) or later to use
  // Start scanning
  private void scanNew(a) {
    BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    // Basic scanning methods
    bluetoothManager
        .getAdapter()
        .getBluetoothLeScanner()
        .startScan(mScanCallback);


    // Set some scan parameters
    ScanSettings settings=new ScanSettings
        .Builder()
        // For example, the low latency mode is set here, which means faster scanning to surrounding devices, and correspondingly more power consumption
        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
        .build();

    // You need to set the filter criteria not just by service UUID as in the old API
    // Filter by device name, MAC address, etc
    List<ScanFilter> scanFilters=new ArrayList<>();

    // If you need to filter the scanned devices, you can use the following constructor
    bluetoothManager
        .getAdapter()
        .getBluetoothLeScanner()
        .startScan(scanFilters,settings,mScanCallback);
  }

  // Scan result callback
  ScanCallback mScanCallback = new ScanCallback() {
     @Override
     public void onScanResult(int callbackType, ScanResult result) {
        //callbackType: scan mode
        //result: scanned device data, including Bluetooth device objects, parsed broadcast data, etc}};// Stop scanning
  private void stopNewScan(a){
    BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    bluetoothManager.getAdapter().getBluetoothLeScanner().stopScan(mScanCallback);
  }
Copy the code

Compared with the old API, the new API has more comprehensive functions, but it requires Android 5.0 or above to use, which method you need to use, you can choose according to your actual situation.

Watch out for potholes:

  • 1. If the device cannot be found, check whether the ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission for Android 6.0 or later has been dynamically granted, and check whether the location information (i.e. GPS) has been enabled. There are two reasons why you can’t find a device.

  • 2. Both the old and new API scans the callback is constantly scanning to callback device, even with the same equipment is to be repeated correction, until you stop scanning, so it is best not to do too much in the callback method time-consuming operation, otherwise it may appear this problem, if need to handle the callback data can put data in another thread processing, Let the callback return as soon as possible.

The connection

We can only connect to one peripheral device at a time. If we need to connect to multiple devices, we can wait for one connection to succeed before making the next connection. Otherwise, if one of the previous connection operations fails without a callback, the subsequent operations will always be blocked.

  // Initiate a connection
  private void connect(BluetoothDevice device){
    mBluetoothGatt = device.connectGatt(context, false, mBluetoothGattCallback);
  }

  //Gatt operation callback, this callback is important, all subsequent operation results will be callback in this method
  BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
     @Override
     public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
       / / gatt: gatt client
       //status: indicates the status code of the operation. If 0 is returned, the operation is successful. If other values are returned, the operation is abnormal
       //newState: indicates the state of the current connection, for example, the connection is successful or disconnected

       // Trigger this callback when the connection state changes
     }

     @Override
     public void onServicesDiscovered(BluetoothGatt gatt, int status) {
       / / gatt: gatt client
       //status: indicates the status code of the operation. If 0 is returned, the operation is successful. If other values are returned, the operation is abnormal

       // This callback is triggered when a service is successfully acquired, as described in the section "Obtaining a service, Characteristics"
     }

     @Override
     public void onCharacteristicRead(BluetoothGatt gatt,
         final BluetoothGattCharacteristic characteristic, final int status) {
           / / gatt: gatt client
           //status: indicates the status code of the operation. If 0 is returned, the operation is successful. If other values are returned, the operation is abnormal
           //characteristic

           // This callback is triggered when the read operation on a trait is complete, as described in the "Read Trait" section
     }

     @Override
     public void onCharacteristicWrite(BluetoothGatt gatt,
         final BluetoothGattCharacteristic characteristic, final int status) {
           / / gatt: gatt client
           //status: indicates the status code of the operation. If 0 is returned, the operation is successful. If other values are returned, the operation is abnormal
           //characteristic

           // This callback is triggered when a write to a trait is complete, as described in the "Write Trait" section
     }

     @Override
     public void onCharacteristicChanged(BluetoothGatt gatt,
         final BluetoothGattCharacteristic characteristic) {
           / / gatt: gatt client
           //status: indicates the status code of the operation. If 0 is returned, the operation is successful. If other values are returned, the operation is abnormal
           //characteristic

           // This callback is triggered when eigenvalues change, as described in the "Opening notifications" section
     }

     @Override
     public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
         int status) {
           / / gatt: gatt client
           //status: indicates the status code of the operation. If 0 is returned, the operation is successful. If other values are returned, the operation is abnormal
           // Descriptor: Read descriptor

           Triggered when the read operation to descriptor is complete
     }

     @Override
     public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
         int status) {
           / / gatt: gatt client
           //status: indicates the status code of the operation. If 0 is returned, the operation is successful. If other values are returned, the operation is abnormal
           // Descriptor: Descriptor to be written

           // Triggered when a write to descriptor completes, as described in the section "Opening notifications"}};Copy the code

The onConnectionStateChange callback is triggered when the connectGatt method is called. The status callback is used to determine the success of the operation and the newState callback is used to determine the current connection status.

Watch out for potholes:

  • It is best to call the connect and disconnect methods on the main thread, otherwise we may encounter strange problems on some phones

Get services, features

After the connection is successful, the GATT client (mobile phone A) can retrieve the services and features of the GATT server (mobile phone B) by discovery method for later use.

  // The discovery service was used after the connection was successful
  gatt.discoverServices();

      // This method is called back when the service retrieval is complete, and we can retrieve the required services and characteristics
      @Override
      public void onServicesDiscovered(BluetoothGatt gatt, int status) {

        // Get the service for a specific UUID
        BluetoothGattService service = gatt.getService(UUID_SERVER);

        // Get all services
        List<BluetoothGattService> services = gatt.getServices();

        if(service! =null) {// Get the characteristics of a specific UUID under the service
          mCharacteristic = service.getCharacteristic(UUID_CHARWRITE);

          // Get all the features under the serviceList<BluetoothGattCharacteristic> characteristics = service.getCharacteristics(); }}Copy the code

Open the notification

The standard practice of opening notification to the authorities is in two steps:

// Official documentation
private BluetoothGatt mBluetoothGatt;
BluetoothGattCharacteristic characteristic;
booleanenabled; .// The first step is to enable notification of this feature on mobile phone A(local)mBluetoothGatt.setCharacteristicNotification(characteristic, enabled); .// The second step is to enable notifications by writing the enable notification command to the CCCD of phone B(remote) for the feature for which notifications need to be enabled
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
        UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
Copy the code

There was a bug in previous versions of Android7.0 that writes to descriptor would reuse the write type of the parent feature. This bug was fixed after 7.0. In order to improve compatibility, we can modify the official method slightly:

private BluetoothGatt mBluetoothGatt;
BluetoothGattCharacteristic characteristic;
booleanenabled; .// The first step is to enable notification of this feature on mobile phone A(local)mBluetoothGatt.setCharacteristicNotification(characteristic, enabled); .// The second step is to enable notifications by writing the enable notification command to the CCCD of phone B(remote) for the feature for which notifications need to be enabled
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
        UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
// Get the write type of the feature for later restoration
int parentWriteType = characteristic.getWriteType();
// Set the write type of the feature to the default type
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
// Restore the write type of the feature
characteristic.setWriteType(parentWriteType);
Copy the code

Next we look at callbacks

      @Override
      public void onCharacteristicChanged(BluetoothGatt gatt,
          final BluetoothGattCharacteristic characteristic) {
          // This callback is triggered when a notification from phone B is sent
      }

      @Override
      public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
          int status) {
        // The second step triggers the callback
      }
Copy the code

Note:

  • For some devices we may only need to perform the first step to receive notifications, but to be on the safe side it is best to do both steps in case notifications turn on and off.
  • The GATT operations, such as read, write, and notification, can only be used serially. Before executing the next task, ensure that the previous task has been completed and the callback is successful; otherwise, the subsequent tasks may be blocked.
  • The next GATT operation can be performed to indicate task completion when the notification operation triggers onDescriptorWrite.

Writing characteristics

// Default write type that requires peripheral response
mCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
// Write type without device response
mCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);

mCharacteristic.setValue(data);
mBluetoothGatt.writeCharacteristic(mCharacteristic);


      // Write the feature callback
      @Override
      public void onCharacteristicWrite(BluetoothGatt gatt,
          final BluetoothGattCharacteristic characteristic, final int status) {}Copy the code

The use of write features is similar to that of the write Descriptor in the previous open notifications.

Note:

  • The two types of write mentioned above are as follows:
    • WRITE_TYPE_DEFAULT: Callback onCharacteristicWrite requires a response from the peripheral
    • WRITE_TYPE_NO_RESPONSE: Callback onCharacteristicWrite without a response from the peripheral

If you write with type WRITE_TYPE_DEFAULT and the peripheral does not respond, all subsequent operations will be blocked. So, depending on your peripherals, you can try to comment out this line in the sample project and then write the data again, and see if you can understand it better with the log.

  • Up to 20 bytes of data can be written at a time, multiple writes can be subcontracted if more data needs to be written, or up to 512 bytes can be transferred at a time if the device supports changing the MTU.

Read characteristics

/ / read characteristics
mBluetoothGatt.readCharacteristic(mCharacteristic);

// Read the feature's callback
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
          final BluetoothGattCharacteristic characteristic, final int status) {}Copy the code

Read characteristics is not a bad operation, it just needs the successful callback mentioned earlier to complete

disconnect

private void disConnect(a){
    if(mBluetoothGatt! =null) {// Disconnect the connection
      mBluetoothGatt.disconnect();
      // mBluetoothGatt.close();}}@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
    if (newState==BluetoothProfile.STATE_DISCONNECTED){
        // Close the GATT clientgatt.close(); }}Copy the code

Note:

  • Disconnecting, like connecting, is best done on the main thread
  • The bluetoothgat.disconnect () method and the bluetoothgat.close () method need to be used in pairs with one note: If the close() method is called immediately after calling the disConnect() method (as in the code commented out above) bluetooth disconnects normally, It is only in onConnectionStateChange that we do not receive a state callback with newState being bluetoothprofile.state_disconnected, so we can shut down the GATT client after receiving a disconnected callback.
  • If the close method is not called after disconnecting, you may not be able to connect to the device again after repeated connection-disconnection.

conclusion

In addition to all this article lists some actually use API and possible problems, the most main is to emphasize a bluetooth operation rhythm, which is a task to complete the next task to start principle, in order to facilitate everybody introduction, the above using simplified a lot of logic, to consider such as: read, write, notice hasn’t been back? (You can add timeouts to all of these operations) and so on, but you can avoid many of the strange problems you might encounter if you follow the methods provided in this article.

If you need to know more detailed usage methods, here are two open source BLE libraries for you to recommend:

  • Android – BLE – Library: NordicSemiconductor official Android BLE Library.
  • BLELib: I packaged ble library, you can feel free to star if you like.