Monday, December 21, 2015

Connect HM-10 (BLE Module) to Android device, with BluetoothLeGatt sample project

Android 4.3 (API Level 18) introduces built-in platform support for Bluetooth Low Energy, or called BLE, Bluetooth 4.

HM-10 is a BLE Bluetooth 4.0 Serial Wireless Module. In this test, nothing to do on HM-10, except apply 3.3V on the power pins.


BluetoothLeGatt is a sample demonstrates how to use the Bluetooth LE Generic Attribute Profile (GATT) to transmit arbitrary data between devices.



This video show how to import BluetoothLeGatt sample project in Android Studio, and run on Android device, to scan and connect to HM-10.



Then, we are going to modify something to make the sample app recognize HM-10.


Refer to the post "Test HM-10 Bluetooth 4.0 BLE module with FTDI adapter", the default service UUID and Characteristic of HM-10 are:
- Service UUID: 0xFFE0
- Characteristic: 0xFFE1

Edit SampleGattAttributes.java of BluetoothLeGatt sample to match 0xffe0 and 0xff1, and correct BluetoothLeService.java accordingly. Now, the sample app can recognize HM-10.

This video show how:


SampleGattAttributes.java
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.bluetoothlegatt;

import java.util.HashMap;

/**
 * This class includes a small subset of standard GATT attributes for demonstration purposes.
 */
public class SampleGattAttributes {
    private static HashMap<String, String> attributes = new HashMap();
    public static String HM_10 = "0000ffe1-0000-1000-8000-00805f9b34fb";
    public static String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb";

    static {
        // Sample Services.
        attributes.put("0000ffe0-0000-1000-8000-00805f9b34fb", "HM-10 Service");
        attributes.put("0000180a-0000-1000-8000-00805f9b34fb", "Device Information Service");
        // Sample Characteristics.
        attributes.put(HM_10, "HM-10 Module");
        attributes.put("00002a29-0000-1000-8000-00805f9b34fb", "Manufacturer Name String");
    }

    public static String lookup(String uuid, String defaultName) {
        String name = attributes.get(uuid);
        return name == null ? defaultName : name;
    }
}


BluetoothLeService.java
    public final static UUID UUID_HEART_RATE_MEASUREMENT =
            UUID.fromString(SampleGattAttributes.HM_10);


In the last step in this post, I want to make the BluetoothLeGatt app to receive something from HM-10.

In HM-10 side, FTDI USB-Serial adapter is needed to connect HM-10 to PC via USB/Serial. Such that I can enter something in PC, using Arduino Serial Monitor. Refer Arduino-er: Test HM-10 Bluetooth 4.0 BLE module with FTDI adapter, how to connect PC, FTDI USB-Serial adapter and HM-10.

The original BluetoothLeGatt example target for the Heart Rate Measurement profile. In our demo, we target to receive simple serial data, so modify to by-pass the Heart Rate Measurement profile handling.

Edit broadcastUpdate(final String action, final BluetoothGattCharacteristic characteristic) method of BluetoothLeService.java.

This video show how to:


BluetoothLeService.java
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.bluetoothlegatt;

import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;

import java.util.List;
import java.util.UUID;

/**
 * Service for managing connection and data communication with a GATT server hosted on a
 * given Bluetooth LE device.
 */
public class BluetoothLeService extends Service {
    private final static String TAG = BluetoothLeService.class.getSimpleName();

    private BluetoothManager mBluetoothManager;
    private BluetoothAdapter mBluetoothAdapter;
    private String mBluetoothDeviceAddress;
    private BluetoothGatt mBluetoothGatt;
    private int mConnectionState = STATE_DISCONNECTED;

    private static final int STATE_DISCONNECTED = 0;
    private static final int STATE_CONNECTING = 1;
    private static final int STATE_CONNECTED = 2;

    public final static String ACTION_GATT_CONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_CONNECTED";
    public final static String ACTION_GATT_DISCONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";
    public final static String ACTION_GATT_SERVICES_DISCOVERED =
            "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";
    public final static String ACTION_DATA_AVAILABLE =
            "com.example.bluetooth.le.ACTION_DATA_AVAILABLE";
    public final static String EXTRA_DATA =
            "com.example.bluetooth.le.EXTRA_DATA";

    public final static UUID UUID_HEART_RATE_MEASUREMENT =
            UUID.fromString(SampleGattAttributes.HM_10);

    // Implements callback methods for GATT events that the app cares about.  For example,
    // connection change and services discovered.
    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            String intentAction;
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                intentAction = ACTION_GATT_CONNECTED;
                mConnectionState = STATE_CONNECTED;
                broadcastUpdate(intentAction);
                Log.i(TAG, "Connected to GATT server.");
                // Attempts to discover services after successful connection.
                Log.i(TAG, "Attempting to start service discovery:" +
                        mBluetoothGatt.discoverServices());

            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                intentAction = ACTION_GATT_DISCONNECTED;
                mConnectionState = STATE_DISCONNECTED;
                Log.i(TAG, "Disconnected from GATT server.");
                broadcastUpdate(intentAction);
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
            } else {
                Log.w(TAG, "onServicesDiscovered received: " + status);
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic characteristic,
                                         int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
            }
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt,
                                            BluetoothGattCharacteristic characteristic) {
            broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
        }
    };

    private void broadcastUpdate(final String action) {
        final Intent intent = new Intent(action);
        sendBroadcast(intent);
    }

    private void broadcastUpdate(final String action,
                                 final BluetoothGattCharacteristic characteristic) {
        final Intent intent = new Intent(action);


        /*
        // This is special handling for the Heart Rate Measurement profile.  Data parsing is
        // carried out as per profile specifications:
        // http://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml
        if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
            int flag = characteristic.getProperties();
            int format = -1;
            if ((flag & 0x01) != 0) {
                format = BluetoothGattCharacteristic.FORMAT_UINT16;
                Log.d(TAG, "Heart rate format UINT16.");
            } else {
                format = BluetoothGattCharacteristic.FORMAT_UINT8;
                Log.d(TAG, "Heart rate format UINT8.");
            }
            final int heartRate = characteristic.getIntValue(format, 1);
            Log.d(TAG, String.format("Received heart rate: %d", heartRate));
            intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));
        } else {
            // For all other profiles, writes the data formatted in HEX.
            final byte[] data = characteristic.getValue();
            if (data != null && data.length > 0) {
                final StringBuilder stringBuilder = new StringBuilder(data.length);
                for(byte byteChar : data)
                    stringBuilder.append(String.format("%02X ", byteChar));
                intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString());
            }
        }
        */

        Log.v("AndroidLE", "broadcastUpdate()");

        final byte[] data = characteristic.getValue();

        Log.v("AndroidLE", "data.length: " + data.length);

        if (data != null && data.length > 0) {
            final StringBuilder stringBuilder = new StringBuilder(data.length);
            for(byte byteChar : data) {
                stringBuilder.append(String.format("%02X ", byteChar));

                Log.v("AndroidLE", String.format("%02X ", byteChar));
            }
            intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString());
        }

        sendBroadcast(intent);
    }

    public class LocalBinder extends Binder {
        BluetoothLeService getService() {
            return BluetoothLeService.this;
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        // After using a given device, you should make sure that BluetoothGatt.close() is called
        // such that resources are cleaned up properly.  In this particular example, close() is
        // invoked when the UI is disconnected from the Service.
        close();
        return super.onUnbind(intent);
    }

    private final IBinder mBinder = new LocalBinder();

    /**
     * Initializes a reference to the local Bluetooth adapter.
     *
     * @return Return true if the initialization is successful.
     */
    public boolean initialize() {
        // For API level 18 and above, get a reference to BluetoothAdapter through
        // BluetoothManager.
        if (mBluetoothManager == null) {
            mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
            if (mBluetoothManager == null) {
                Log.e(TAG, "Unable to initialize BluetoothManager.");
                return false;
            }
        }

        mBluetoothAdapter = mBluetoothManager.getAdapter();
        if (mBluetoothAdapter == null) {
            Log.e(TAG, "Unable to obtain a BluetoothAdapter.");
            return false;
        }

        return true;
    }

    /**
     * Connects to the GATT server hosted on the Bluetooth LE device.
     *
     * @param address The device address of the destination device.
     *
     * @return Return true if the connection is initiated successfully. The connection result
     *         is reported asynchronously through the
     *         {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
     *         callback.
     */
    public boolean connect(final String address) {
        if (mBluetoothAdapter == null || address == null) {
            Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
            return false;
        }

        // Previously connected device.  Try to reconnect.
        if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress)
                && mBluetoothGatt != null) {
            Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection.");
            if (mBluetoothGatt.connect()) {
                mConnectionState = STATE_CONNECTING;
                return true;
            } else {
                return false;
            }
        }

        final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
        if (device == null) {
            Log.w(TAG, "Device not found.  Unable to connect.");
            return false;
        }
        // We want to directly connect to the device, so we are setting the autoConnect
        // parameter to false.
        mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
        Log.d(TAG, "Trying to create a new connection.");
        mBluetoothDeviceAddress = address;
        mConnectionState = STATE_CONNECTING;
        return true;
    }

    /**
     * Disconnects an existing connection or cancel a pending connection. The disconnection result
     * is reported asynchronously through the
     * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
     * callback.
     */
    public void disconnect() {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }
        mBluetoothGatt.disconnect();
    }

    /**
     * After using a given BLE device, the app must call this method to ensure resources are
     * released properly.
     */
    public void close() {
        if (mBluetoothGatt == null) {
            return;
        }
        mBluetoothGatt.close();
        mBluetoothGatt = null;
    }

    /**
     * Request a read on a given {@code BluetoothGattCharacteristic}. The read result is reported
     * asynchronously through the {@code BluetoothGattCallback#onCharacteristicRead(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)}
     * callback.
     *
     * @param characteristic The characteristic to read from.
     */
    public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }
        mBluetoothGatt.readCharacteristic(characteristic);
    }

    /**
     * Enables or disables notification on a give characteristic.
     *
     * @param characteristic Characteristic to act on.
     * @param enabled If true, enable notification.  False otherwise.
     */
    public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
                                              boolean enabled) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }
        mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);

        // This is specific to Heart Rate Measurement.
        if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
            BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
                    UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            mBluetoothGatt.writeDescriptor(descriptor);
        }
    }

    /**
     * Retrieves a list of supported GATT services on the connected device. This should be
     * invoked only after {@code BluetoothGatt#discoverServices()} completes successfully.
     *
     * @return A {@code List} of supported services.
     */
    public List<BluetoothGattService> getSupportedGattServices() {
        if (mBluetoothGatt == null) return null;

        return mBluetoothGatt.getServices();
    }
}



Next:
Android echo data to Bluetooth LE device, using HM-10 BLE Module


Another BLE Module, AT-09, is compatible to HM-10:
Modified BluetoothLeGatt sample connect AT-09 (Bluetooth LE Module)

More
Implement dummy Heart Rate Measurement profile using Arduino Due + HM-10, for BluetoothLeGatt sample code


Updated@2017-06-05, about permission:
As Android updated, start Bluetooth LE scan by calling startScan(ScanCallback callback) or startScan(List filters, ScanSettings settings, ScanCallback callback) methods of android.bluetooth.le.BluetoothLeScanner, BLUETOOTH_ADMIN permission is required, also ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission in order to get results.


20 comments:

Unknown said...

Did it work properly for you? I have tried your modification, but It's not receiving data. Only If I use app HMBLE from Google play before, than it's working fine. Looks like there is some truble with creating comunication between Android device and HM-10.Do you have some advice for me please?

Erik said...

hello Matúš Pohančeník,

it work for me, as shown in the videos.

Unknown said...

Do you think there is a diference, when I'm sending data from arduino in loop like this


void setup() {
Serial.begin(9600);
}

void loop()
{
if (Serial.available()) {
Serial.println("sending");

}
delay(1000);
}

or did you make any changes at the HM modul by At commands? I am very new at this, so I don't understand details much.Please If you can help me...

Erik said...

hello Matúš Pohančeník,

In your code, the Arduino check Serial.available() in every one second, so that it will response slowly.

Second, as I remember, Serial.available() check number of bytes available to read, without read. So the data is still available, and always available.

Unknown said...

THANK YOU SO MUCH,
I just deleted Serial.available() from loop and it works perfect.
Thank you again.

dylank said...

Great tutorial! However, when I run the app (with the modifications) and scan the HM-10 is not even visible. When I try to connect in regular Bluetooth settings it is visible but says that it is not ready to pair. Do you happen to know how I can fix this?
Thanks in advanced.

Marvia Edith said...

This tutorial is very helpful, do you have the project in github or something?

Erik said...

Hello Marvia Edith,

Actually it is a example code of Android HERE, with little modification as shown in the post.

I'm preparing my step-by-step example HERE

Unknown said...

Do you have send and receive code for the arduino? Assume I was able to connect the android device to the bluetooth le module, how can I send data from it? (P.S response is not necessary only sending data to the bluetooth module only to be received by the arduino atmega)

Unknown said...

how to extract incoming data from the app, i am trying to plot a graph from the incoming data

Lazar said...

By some new standards you will have to request permission to get users location in order for this code to work.

Erik said...

Thanks Lazar, just added remark on the post text.

chans said...

from some moment, we need to put extra code for using scanning BT.
That is "ACCESS_COARSE_LOCATION"
So, You need to put some code.
1. in your AndroidManifest.xml file, add this line



2. in the DeviceScanActivity.java, add this code inside of onCreate method
int MY_PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION = 1;
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
MY_PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);

it will be look like this
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getActionBar().setTitle(R.string.title_devices);
mHandler = new Handler();
int MY_PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION = 1;
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
MY_PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);

nd I also found that I needed to add the bluetooth device in advance.
I mean, after I registered the bluetooth by using the native android bluetooth configuration window.
I think becuase I needed to put the pincode but this sample application doesn't open any window for putting pincode.
anyway, I am now able to connect the device.

...

Unknown said...

I am considering using an HM-10 BLE module to capture, or receive, the data from a VR remote controller named ACGAM R1, which is a 4.0 bluetooth device. So far, I have not been able to get the HM-10 to even detect the ACGAM presence. Honestly, I thought it would but no I am convinced I am missing something and have no clue what it is. - Do you have an idea why is this happening ? - I found your post very interesting, trying to find an answer to my question. Thank you for your time in advance.

Unknown said...

Use the serial Bluetooth terminal app from appstore then you can get paired else yours module is not good.since HM-10 is a BLE i.e Low Energy is different from other Bluetooth devices your module can get connected only the apps having for scanning Bluetooth LE (Low Energy)

Unknown said...

Try using serial Bluetooth terminal app since Hm-10 is BLE - Low Energy device.

Anonymous said...

Can someone explain me what I exactly have to do with the Update "As Android updated, start Bluetooth LE scan by calling startScan(ScanCallback callback) or startScan(List filters, ScanSettings settings, ScanCallback callback)"? I don´t have any idea what I´ve to change.

Anonymous said...

Eunchan Park, you saved my day ... thank you :)

Anonymous said...

Art Dahm, Andr.oid Eric, Eunchan Park... thanks!

Unknown said...

I faced the problem of not finding any BLE devices using Android 8.
Based on the comment of Eunchan Park, I was able to adapt the file for the usage on my device.

Here's my "updated" BLE GATT sample:
https://github.com/schollp/BLEGattAndroid8