NordicSemiconductor / Android-BLE-Library

A library that makes working with Bluetooth LE on Android a pleasure. Seriously.
BSD 3-Clause "New" or "Revised" License
2.02k stars 417 forks source link

Unable to connect to device after scan and write characteristic #89

Closed Rakesh4a7 closed 2 years ago

Rakesh4a7 commented 5 years ago

Hi,

I'm using Android-Scanner-Compat-Library to scan the data and trying to connect using Android-BLE-Library. I have tried to follow the code provided by Android-nRF-Toolbox. But the problem is it's written using ViewModel pattern and I'm having difficulty to understand and implement in single class.

Can anyone provide an example where the connection part and Read and Write characteristic is written in single class without ViewModel pattern.

Below is the code which i have done till now.

private void connectNow() {
        if (mScanning) {
            // Extend scanning for some time more
            mHandler.removeCallbacks(mStopScanTask);
            mHandler.postDelayed(mStopScanTask, SCAN_DURATION);
            return;
        }
        Intent intent = new Intent(this, MyReceiver.class); // explicite intent
        intent.setAction("com.example.ACTION_FOUND");
        PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 
        PendingIntent.FLAG_UPDATE_CURRENT);

        BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
        ScanSettings settings = new ScanSettings.Builder()
                .setLegacy(false)
                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                .setReportDelay(1000)
                .setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED)
                .build();
        List<ScanFilter> filters = new ArrayList<>();
        ScanFilter filter = new ScanFilter.Builder().setDeviceAddress("00:0B:57:47:D5:EA").build();
        filters.add(filter);
        scanner.startScan(filters, settings, this, pendingIntent);

        // Setup timer that will stop scanning
        mHandler.postDelayed(mStopScanTask, SCAN_DURATION);
        mScanning = true;
    }
public class MyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if(intent.hasExtra(BluetoothLeScannerCompat.EXTRA_LIST_SCAN_RESULT)){
            ArrayList<ScanResult> results = intent.getParcelableArrayListExtra(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT);

            for (final ScanResult result : results) {
                Log.e("onScanResult","onScanResult : " + result.getDevice().getName() + " " + result.getDevice().getAddress());
                Toast.makeText(context, result.getDevice().getName() + " " + result.getDevice().getAddress(), Toast.LENGTH_LONG).show();
            }
        }
    }
philips77 commented 5 years ago

You have to create the manager class that will extend BleManager and handle communication with your device. A simple example you will find here: https://github.com/NordicSemiconductor/Android-nRF-Blinky/blob/master/app/src/main/java/no/nordicsemi/android/blinky/profile/BlinkyManager.java In nRF Blinky it is indeed used from ViewModel, but it doesn't have to be. I would recommend using it from a service (like here: https://github.com/NordicSemiconductor/Android-nRF-Toolbox/tree/master/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc), but you may just directly use it in your Activity. The example is here: https://github.com/NordicSemiconductor/Android-nRF-Toolbox/tree/master/app/src/main/java/no/nordicsemi/android/nrftoolbox/hrs

Rakesh4a7 commented 5 years ago

Hi @philips77 I tried to follow the example https://github.com/NordicSemiconductor/Android-nRF-Blinky/blob/master/app/src/main/java/no/nordicsemi/android/blinky/profile/BlinkyManager.java

But i'm unable to read the data from my device. This is the demo i made using the classes and changed the UUID. https://github.com/Rakesh4a7/BLEDemo

Can you help me out.

What i want to achieve?

  1. Connect to BLE device.
  2. write command to BLE for broadcasting data
  3. Receive data broadcast by BLE device

Below is my working code which i want to implement using NRF library.

public void connectToBleDevice(final String deviceId) {
        String tempId = "";
        for (String s : profile.getBLEDeviceIDs()) {
            tempId = s.replace(":", "");
            if (tempId.toLowerCase().contentEquals(deviceId)) {
                tempId = s;
                break;
            }
        }

        if (!tempId.isEmpty()) {
            this.deviceId = deviceId;
            device = btAdapter.getRemoteDevice(tempId);
            stopScanning();
            device.connectGatt(mContext, true, mBluetoothGattCallback);
        }
}

BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);

            switch (newState) {
                case BluetoothAdapter.STATE_CONNECTED:
                    gatt.discoverServices();
                    break;
                case BluetoothAdapter.STATE_CONNECTING:
                    break;
                case BluetoothAdapter.STATE_DISCONNECTED:
            gatt.disconnect();
                    // Close the gatt
                    gatt.close();
                    startScanning();
                    break;
                case BluetoothAdapter.STATE_DISCONNECTING:
                    break;
                default:
                    break;
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);

            BluetoothGattService dataTransferService = gatt.getService(dataTransferServiceUUID);
            BluetoothGattCharacteristic responseCharacteristic = dataTransferService.getCharacteristic(responseCharacteristicUUID);
            gatt.setCharacteristicNotification(responseCharacteristic, true);
            BluetoothGattDescriptor descriptor = responseCharacteristic.getDescriptor(clientCharacteristicConfiguration);

            // Enable notification
            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);

            // Write the descriptor to the gatt
            gatt.writeDescriptor(descriptor);
        }

        @Override
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorWrite(gatt, descriptor, status);

            // Now that notification is enabled we can request.
            BluetoothGattCharacteristic requestCharacteristic = gatt.getService(dataTransferServiceUUID).getCharacteristic(requestCharacteristicUUID);

            // Now set and write the History value
            requestCharacteristic.setValue(new byte[]{(byte) 0x03});
            gatt.writeCharacteristic(requestCharacteristic);
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);
            java.util.Formatter formatter = new java.util.Formatter();
            for (byte b : characteristic.getValue()) {
                formatter.format("%02x", b);
            }
            String hex = formatter.toString();
            if (hex.substring(0, 2).contentEquals("83")) {
                // Perform operation with data 
            }
        }
};
philips77 commented 5 years ago

Hello, I merged a bit what you have here and there, and here's the initialize() method, that could work for you:

@Override
protected void initialize() {
     // Receive data broadcast by BLE device
     setNotificationCallback(responseCharacteristic).with((device, data)  -> {
            java.util.Formatter formatter = new java.util.Formatter();
            for (byte b : data.getValue()) {
                formatter.format("%02x", b);
            }
            String hex = formatter.toString();
            if (hex.substring(0, 2).contentEquals("83")) {
                // Perform operation with data 
            }
     });
     enableNotifications(responseCharacteristic).enqueue();
     // Write command to BLE for broadcasting data
     writeCharacteristic(requestCharacteristic, new byte[] { (byte) 0x03 }).enqueue();
}

The 0x03 request will be sent automatically with this code. If you want it to be triggered manually, or some time later, just remove the last writeCharacteristic... line and use your send() method.

Also, it depends when do you want to get your connect success callback. With the above code you will get it after the request has been written (and the notification enabled before). But you may also want to get it after you receive the notification from the device. In that case, instead of setNotificationCallback(...) you may use waitForNotification and write the request as a trigger.

Rakesh4a7 commented 5 years ago

Hi @philips77

I tried the above but request characteristic is always Null. So, unable to call send request.

when isRequiredServiceSupported() method is called?

public class MainActivity extends AppCompatActivity implements BleManagerCallbacks, BlinkyManagerCallbacks {
    public final String TAG = "MainActivity";

    Context mContext = null;
    TextView data;
    private final List<BluetoothDevice> mDevices = new ArrayList<>();
    /** Flag set to true when scanner is active. */
    private boolean mScanning;
    private Handler mHandler;
    private final static long SCAN_DURATION = 5000;
    private boolean mDeviceConnected = false;

    private BlinkyManager mBlinkyManager;
    private BluetoothDevice mDevice;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mContext = this;

        // Initialize the manager
        mBlinkyManager = new BlinkyManager(this);
        mBlinkyManager.setGattCallbacks(this);

        final Button connect = findViewById(R.id.button);
        data = findViewById(R.id.data);
        mHandler = new Handler();

        connect.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ensureBLESupported();
                if (!isBLEEnabled()) {
                    showBLEDialog();
                    return;
                }
                connectNow();
            }
        });
    }

    private void ensureBLESupported() {
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            Toast.makeText(this, "BLE not supported", Toast.LENGTH_LONG).show();
            finish();
        }
    }

    protected boolean isBLEEnabled() {
        final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        final BluetoothAdapter adapter = bluetoothManager.getAdapter();
        return adapter != null && adapter.isEnabled();
    }

    protected void showBLEDialog() {
        final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableIntent, 1);
    }

    private void connectNow() {
        if (mScanning) {
            // Extend scanning for some time more
            mHandler.removeCallbacks(mStopScanTask);
            mHandler.postDelayed(mStopScanTask, SCAN_DURATION);
            return;
        }

       /* Intent intent = new Intent(this, MyReceiver.class); // explicite intent
        intent.setAction("com.example.ACTION_FOUND");
        PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
*/

        BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
        ScanSettings settings = new ScanSettings.Builder()
               // .setLegacy(false)
                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                //.setReportDelay(1000)
               // .setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED)
                .build();
        List<ScanFilter> filters = new ArrayList<>();
        ScanFilter filter = new ScanFilter.Builder().setDeviceAddress("00:0B:57:47:D5:EA").build();
        filters.add(filter);
        scanner.startScan(filters, settings, mScanCallback);

        // Setup timer that will stop scanning
        mHandler.postDelayed(mStopScanTask, SCAN_DURATION);
        mScanning = true;
    }

    public void stopLeScan() {
        if (!mScanning)
            return;

        final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
        scanner.stopScan(mScanCallback);

        mHandler.removeCallbacks(mStopScanTask);
        mScanning = false;
    }

    private Runnable mStopScanTask = new Runnable() {
        @Override
        public void run() {
            stopLeScan();
        }
    };

    private ScanCallback mScanCallback = new ScanCallback() {
        @Override
        public void onScanResult(final int callbackType, final ScanResult result) {
            // empty
            Log.e("onScanResult","onScanResult : " + result.getDevice().getName() + " " + result.getDevice().getAddress());
            BluetoothDevice device = result.getDevice();
            if(!mDeviceConnected){
                mDevice = device;
                reconnect();
                data.setText(data.getText() +"\n"+ device.getAddress());
            }
        }

        @Override
        public void onBatchScanResults(final List<ScanResult> results) {
            final int size = mDevices.size();
            BluetoothDevice device;
            for (final ScanResult result : results) {
                device = result.getDevice();
                if(!mDeviceConnected){
                    mDevice = device;
                    reconnect();
                    data.setText(data.getText() +"\n"+ device.getAddress());
                }

                if (!mDevices.contains(device))
                    mDevices.add(device);

                if (size != mDevices.size()) {
                    if(!mDeviceConnected){
                        mDevice = device;
                        reconnect();
                    }
                    data.setText(data.getText() +"\n"+ device.getAddress());
                }
            }

        }

        @Override
        public void onScanFailed(final int errorCode) {
            // empty
        }
    };

    @Override
    protected void onPause() {
        super.onPause();
        stopLeScan();
        if(mDeviceConnected)
            disconnect();
    }

    /**
     * Reconnects to previously connected device.
     * If this device was not supported, its services were cleared on disconnection, so
     * reconnection may help.
     */
    public void reconnect() {
        stopLeScan();
        if (mDevice != null) {
            mBlinkyManager.connect(mDevice)
                    .retry(3, 100)
                    .useAutoConnect(true)
                    .enqueue();
        }
    }

    /**
     * Disconnect from peripheral.
     */
    private void disconnect() {
        mDevice = null;
        mBlinkyManager.disconnect().enqueue();
    }

    @Override
    public void onDeviceConnecting(@NonNull BluetoothDevice device) {

    }

    @Override
    public void onDeviceConnected(@NonNull BluetoothDevice device) {
        mDeviceConnected = true;
        mBlinkyManager.send();
    }

    @Override
    public void onDeviceDisconnecting(@NonNull BluetoothDevice device) {

    }

    @Override
    public void onDeviceDisconnected(@NonNull BluetoothDevice device) {
        mDeviceConnected = false;
        connectNow();
    }

    @Override
    public void onLinkLossOccurred(@NonNull BluetoothDevice device) {
        mDeviceConnected = false;
    }

    @Override
    public void onServicesDiscovered(@NonNull BluetoothDevice device, boolean optionalServicesFound) {
        mBlinkyManager.send();
    }

    @Override
    public void onDeviceReady(@NonNull BluetoothDevice device) {

    }

    @Override
    public void onBondingRequired(@NonNull BluetoothDevice device) {

    }

    @Override
    public void onBonded(@NonNull BluetoothDevice device) {

    }

    @Override
    public void onBondingFailed(@NonNull BluetoothDevice device) {

    }

    @Override
    public void onError(@NonNull BluetoothDevice device, @NonNull String message, int errorCode) {

    }

    @Override
    public void onDeviceNotSupported(@NonNull BluetoothDevice device) {

    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK && requestCode == 1) {
            // Make sure we have access coarse location enabled, if not, prompt the user to enable it
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                if (mContext.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {

                    if (this.mContext != null) {
                        final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
                        builder.setTitle("This app needs location access");
                        builder.setMessage("Please grant location access so this app can detect your bluetooth device.");
                        builder.setPositiveButton(android.R.string.ok, null);
                        builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
                            @Override
                            public void onDismiss(DialogInterface dialog) {
                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                                    ((Activity) mContext).requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 125);
                                }
                            }
                        });
                        builder.show();
                    }
                } else {// Permission Granted
                    // Start Scanning
                    connectNow();
                }
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @android.support.annotation.NonNull String[] permissions, @android.support.annotation.NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 125) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

                connectNow();
            }
        }
    }
}
public class BlinkyManager extends BleManager<BlinkyManagerCallbacks> {
    private BluetoothGattCharacteristic requestCharacteristic;
    private BluetoothGattCharacteristic responseCharacteristic;
    private boolean mSupported;

    // Create the uuid for data transfer service
    final UUID dataTransferServiceUUID = UUID.fromString("0b61c398-7697-4762-82d1-5bf490ce0a31");

    // Create the uuid's for the request and response characteristics
    final UUID requestCharacteristicUUID = UUID.fromString("0b61c399-7697-4762-82d1-5bf490ce0a31");
    final UUID responseCharacteristicUUID = UUID.fromString("0b61c39a-7697-4762-82d1-5bf490ce0a31");

    // Create UUID for the Client Characteristic Configuration for the response characteristic
    final UUID clientCharacteristicConfiguration = UUID.fromString( "00002902-0000-1000-8000-00805F9B34FB");

    public BlinkyManager(@NonNull final Context context) {
        super(context);
    }

    @NonNull
    @Override
    protected BleManagerGattCallback getGattCallback() {
        return mGattCallback;
    }

    @Override
    protected boolean shouldClearCacheWhenDisconnected() {
        return !mSupported;
    }

    /**
     * BluetoothGatt callbacks object.
     */
    private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() {
        @Override
        protected void initialize() {
            // Receive data broadcast by BLE device
            setNotificationCallback(responseCharacteristic).with((device, data) -> {
                java.util.Formatter formatter = new java.util.Formatter();
                for (byte b : data.getValue()) {
                    formatter.format("%02x", b);
                }
                String hex = formatter.toString();
                if (hex.substring(0, 2).contentEquals("83")) {
                    // Perform operation with data
                    Log.d("data", hex);
                }
            });
            enableNotifications(responseCharacteristic).enqueue();
        }

        @Override
        public boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) {
            final BluetoothGattService service = gatt.getService(dataTransferServiceUUID);
            if (service != null) {
                responseCharacteristic = service.getCharacteristic(responseCharacteristicUUID);
                requestCharacteristic = service.getCharacteristic(requestCharacteristicUUID);
            }

            boolean writeRequest = false;
            if (responseCharacteristic != null) {
                final int rxProperties = responseCharacteristic.getProperties();
                writeRequest = (rxProperties & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0;
            }

            mSupported = responseCharacteristic != null && writeRequest;
            return mSupported;
        }

        @Override
        protected void onDeviceDisconnected() {
            responseCharacteristic = null;
            requestCharacteristic = null;
        }
    };

    /**
     * Sends a request to the device to turn the LED on or off.
     *
     */
    public void send() {
        // Are we connected?
        if (requestCharacteristic == null)
            return;

        // Write command to BLE for broadcasting data
        writeCharacteristic(requestCharacteristic, new byte[] { (byte) 0x03 }).enqueue();
    }
}
philips77 commented 5 years ago

isRequiredServiceSupported(BluetoothGatt) is called when the device got connected and service discovery has finished. If your requestCharacteristic is null, check if it's present in the device and if the UUID you specified is correct. You may use nRF Connect to see what services and characteristics are on the device.

Your code looks OK.

Rakesh4a7 commented 5 years ago

Hi @philips77

I'm using the same characteristic which is working fine with native android code. But not with NRF library.

Do i need to call the send method from onServicesDiscovered() inspite of onDeviceConnected().

philips77 commented 5 years ago

No, You may call it from wherever. Put a breakpoint in isRequiredServiceSupported and check why the second service you get is null.

philips77 commented 5 years ago

Also, in the method of yours;

@Override
public boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) {
    final BluetoothGattService service = gatt.getService(dataTransferServiceUUID);
    if (service != null) {
        responseCharacteristic = service.getCharacteristic(responseCharacteristicUUID);
        requestCharacteristic = service.getCharacteristic(requestCharacteristicUUID);
    }

    boolean writeRequest = false;
    if (responseCharacteristic != null) { // <- HERE
        final int rxProperties = responseCharacteristic.getProperties();
        writeRequest = (rxProperties & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0;
    }

    mSupported = responseCharacteristic != null && writeRequest;
    return mSupported;
}

in the second if statement, you are checkign the responseCharacteristic for WRITE property. Is that correct? I guess this one should have rather NOTIFY, and requestCharacteristic the WRITE one. Or I don't get something?

philips77 commented 2 years ago

I'm closing the issue due to inactivity.