androidbluetooth-lowenergygattbluetooth-gattcharacteristics

onCharacteristicChanged not being hit. Android BLE


I have an app that is using BLE connectivity with my device. Everything works from connecting to the device, discovering the services, writing the characteristic and descriptor. But for some reason, onCharacteristicChanged is not triggering. The goal is to retrieve the data from the characteristic that is in the characteristic. I have tried using a different characteristic from the same service and this works as in retrieving the data. Not sure why this specific characteristic is not working when others are.

Here is my code: //Descriptor public final static UUID UUID_CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG);

// Huneo Service
public final static UUID UUID_HUNEO_SERVICE = UUID.fromString(GattAttributes.HUNEO_SERVICE);
public final static UUID UUID_HUNEO_VIBRATOR = UUID.fromString(GattAttributes.HUNEO_VIBRATOR);
public final static UUID UUID_HUNEO_MONITOR = UUID.fromString(GattAttributes.HUNEO_MONITOR);
public final static UUID UUID_HUNEO_RATES = UUID.fromString(GattAttributes.HUNEO_RATES);
public final static UUID UUID_HUNEO_LED = UUID.fromString(GattAttributes.HUNEO_LED);
public final static UUID UUID_HUNEO_ACCEL = UUID.fromString(GattAttributes.HUNEO_ACCEL);
public final static UUID UUID_HUNEO_GYRO = UUID.fromString(GattAttributes.HUNEO_GYRO);
public final static UUID UUID_HUNEO_COMBINED = UUID.fromString(GattAttributes.HUNEO_COMBINED);

// Battery Service
public final static UUID UUID_BATTERY_SERVICE = UUID.fromString(GattAttributes.BATTERY_SERVICE);
public final static UUID UUID_BATTERY_LEVEL = UUID.fromString(GattAttributes.BATTERY_LEVEL);

// Device Info Service
public final static UUID UUID_DEVICE_INFO_SERVICE = UUID.fromString(GattAttributes.DEVICE_INFO_SERVICE);
public final static UUID UUID_MODEL_NUMBER = UUID.fromString(GattAttributes.MODEL_NUMBER);
public final static UUID UUID_SERIAL_NUMBER = UUID.fromString(GattAttributes.SERIAL_NUMBER);
public final static UUID UUID_FIRMWARE_REV = UUID.fromString(GattAttributes.FIRMWARE_REV);
public final static UUID UUID_HARDWARE_REV = UUID.fromString(GattAttributes.HARDWARE_REV);
public final static UUID UUID_MANUFACTURER_NAME = UUID.fromString(GattAttributes.MANUFACTURER_NAME);

private final static String TAG = "UNITY";

private static final int STATE_DISCONNECTED = 0;
private static final int STATE_CONNECTING = 1;
private static final int STATE_CONNECTED = 2;
private final BluetoothDevice mDevice;
private final IBinder mBinder = new LocalBinder();

private TaskCompletionSource<Void> mConnectionTask = new TaskCompletionSource<>();
private TaskCompletionSource<String> mFirmwareRevTask = new TaskCompletionSource<>();
private TaskCompletionSource<Void> mServicesDiscovered = new TaskCompletionSource<>();
private TaskCompletionSource<Integer> mBatteryLevelTask = new TaskCompletionSource<>();
private TaskCompletionSource<SensorValues> mStartStreamingTask = new TaskCompletionSource<>();
private TaskCompletionSource<String> mSerialNumberTask = new TaskCompletionSource<>();

private SensorValues mSensorValues = new SensorValues();
private BluetoothGatt mBluetoothGatt;
private int mConnectionState = STATE_DISCONNECTED;
private boolean mIsStreaming = false;

// 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) {
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            mConnectionState = STATE_CONNECTED;
            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) {
            mConnectionState = STATE_DISCONNECTED;
            mIsStreaming = false;
            Log.i(TAG, "Disconnected from GATT server.");
        }
    }

    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            mServicesDiscovered.setResult(null);
            startStreaming();
            Log.w(TAG, "onServicesDiscovered success.");
        } else {
            Log.w(TAG, "onServicesDiscovered received: " + status);
        }
    }

    @Override
    public void onCharacteristicRead(BluetoothGatt gatt,
                                     BluetoothGattCharacteristic characteristic,
                                     int status) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            String characteristicUuid = characteristic.getUuid().toString();
            if (characteristicUuid.equals(GattAttributes.FIRMWARE_REV)) {
                String firmwareRev = characteristic.getStringValue(0);
                mFirmwareRevTask.setResult(firmwareRev);
            }

            if (characteristicUuid.equals(GattAttributes.BATTERY_LEVEL)) {
                int batteryLevel = characteristic.getIntValue(FORMAT_UINT8, 0);
                mBatteryLevelTask.setResult(batteryLevel);
            }

            if (characteristicUuid.equals(GattAttributes.SERIAL_NUMBER)) {
                String serialNumber = characteristic.getStringValue(0);
                mSerialNumberTask.setResult(serialNumber);
            }
        }
    }

    @Override
    public void onCharacteristicWrite(BluetoothGatt gatt,
                                      BluetoothGattCharacteristic characteristic,
                                      int status) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            if (characteristic.getUuid().equals(UUID_HUNEO_MONITOR)) {
                if (characteristic.getIntValue(FORMAT_UINT8, 0) == 1) {
                    startCombined();
                } else {
                    stopCombined();
                }
            }
        }
    }

    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt,
                                        BluetoothGattCharacteristic characteristic) {
        String characteristicUuid = characteristic.getUuid().toString();
        //Combined value changed
        if (characteristicUuid.equals(GattAttributes.HUNEO_COMBINED)) {
            byte[] data = characteristic.getValue();
            int id = unsignedShortToInt(data[0], data[1]);
            int x = unsignedShortToInt(data[2], data[3]);
            int y = unsignedShortToInt(data[4], data[5]);
            int z = unsignedShortToInt(data[6], data[7]);

            mSensorValues.combined = new CartesianValues(id,x,y,z);
            mStartStreamingTask.setResult(mSensorValues);
        }
    }

    @Override
    public void onDescriptorWrite(BluetoothGatt gatt,
                                  BluetoothGattDescriptor descriptor,
                                  int status) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            //Combined
            if (descriptor.getCharacteristic().getUuid().equals(UUID_HUNEO_COMBINED)) {
                mIsStreaming = Arrays.equals(descriptor.getValue(), BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                mStartStreamingTask.setResult(null);
            }
        }
    }
};

public AlwaysOnHuneoBoard(BluetoothDevice device) {
    this.mDevice = device;
}

@Nullable
@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);
}

public static class LocalBinder extends Binder {

}

/**
 * Connect to the device
 *
 * @param ct CancellationToken that will make task return if cancelled
 * @return when task is complete the device is connected
 */
public Task<Void> connectAsync(CancellationToken ct) {
    if (ct.isCancellationRequested()) {
        Log.w(TAG, "Connection cancelled");
        mConnectionTask.setCancelled();
        return mConnectionTask.getTask();
    }

    if (mDevice == null) {
        Log.w(TAG, "BluetoothDevice not initialized");
        mConnectionTask.setError(new Exception("BluetoothDevice not initialized"));
        return mConnectionTask.getTask();
    }

    // Previously connected device. BluetoothGatt not closed. Try to reconnect.
    if (mBluetoothGatt != null) {
        Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection.");
        if (mBluetoothGatt.connect()) {
            mConnectionState = STATE_CONNECTING;
            mConnectionTask.setResult(null);
            return mConnectionTask.getTask();
        }
    }

    mBluetoothGatt = mDevice.connectGatt(this, false, mGattCallback);
    Log.d(TAG, "Trying to create a new connection.");
    mConnectionState = STATE_CONNECTING;
    mConnectionTask.setResult(null);
    return mConnectionTask.getTask();
}

/**
 * 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 (mDevice == null || mBluetoothGatt == null) {
        Log.w(TAG, "BluetoothDevice 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;
}

/**
 * Determines if connected to the BLE device.
 *
 * @return true if connected, false otherwise.
 */
public boolean isConnected() {
    return mConnectionState == STATE_CONNECTED;
}

/**
 * Get the calibration for the sensor
 *
 * @return integer representation of the calibration
 */
public int getCalibration() {
    return 100;
}

/**
 * Get the firmware version of the sensor
 *
 * @return string representing the devices firmware version
 */
public String getFirmwareVersion() {
    try {
        // Wait for services to be discovered
        mServicesDiscovered.getTask().onSuccessTask(t -> {
            BluetoothGattService deviceInfoService = mBluetoothGatt.getService(UUID_DEVICE_INFO_SERVICE);
            BluetoothGattCharacteristic characteristic = deviceInfoService.getCharacteristic(UUID_FIRMWARE_REV);
            mBluetoothGatt.readCharacteristic(characteristic);
            mFirmwareRevTask.setResult(null);
            return mFirmwareRevTask.getTask();

            // Wait for deviceRevTask to be completed
        }).waitForCompletion(10, TimeUnit.SECONDS);

    } catch (InterruptedException e) {
        return "";
    }

    return mFirmwareRevTask.getTask().getResult();
}

/**
 * Get the device mac address
 *
 * @return string representation of the mac address
 */
public String getMacAddress() {
    return mDevice.getAddress();
}

/**
 * Get the model of the device
 *
 * @return string representation of the device model
 */
public String getModel() {
    return mDevice.getName();
}

public SensorValues getCombinedSensorValues() {
    if (!isConnected()) {
        Log.i("Unity", "Sensor disconnected, using default sensor values.");
        return mSensorValues;
    }
    if (!mIsStreaming) {
        Log.i("Unity", "Sensor not streaming, using default sensor values.");
        return mSensorValues;
    }
    try {
        mStartStreamingTask.getTask().waitForCompletion();
    } catch (InterruptedException ex) {
        ex.printStackTrace();
        disconnect();
    }
    return mSensorValues;
}

/**
 * Read the current battery level from the device
 *
 * @return Task containing the sensors battery level as an integer
 */
public Task<Integer> getBatteryLevelAsync() {
    // Wait for services to be discovered
    return mServicesDiscovered.getTask().onSuccessTask(t -> {
        BluetoothGattService deviceInfoService = mBluetoothGatt.getService(UUID_BATTERY_SERVICE);
        BluetoothGattCharacteristic characteristic = deviceInfoService.getCharacteristic(UUID_BATTERY_LEVEL);
        mBluetoothGatt.readCharacteristic(characteristic);
        return mBatteryLevelTask.getTask();
    });
}

public Task<String> getSerialNumber() {
    return mServicesDiscovered.getTask().onSuccessTask(t -> {
        BluetoothGattService deviceInfoService = mBluetoothGatt.getService(UUID_DEVICE_INFO_SERVICE);
        BluetoothGattCharacteristic characteristic = deviceInfoService.getCharacteristic(UUID_SERIAL_NUMBER);
        mBluetoothGatt.readCharacteristic(characteristic);
        return mSerialNumberTask.getTask();
    });
}
/**
 * Issue command to sensor to begin streaming combined data
 */
public void startStreaming() {
    if (mIsStreaming) {
        return;
    }
    //Enable notifications for combined, set result
    //Write 0 to Monitor Characteristic
    BluetoothGattService huneoService = mBluetoothGatt.getService(UUID_HUNEO_SERVICE);
    BluetoothGattCharacteristic monitorCharacteristic = huneoService.getCharacteristic(UUID_HUNEO_MONITOR);
    monitorCharacteristic.setValue(1, FORMAT_UINT8, 0);
    mBluetoothGatt.writeCharacteristic(monitorCharacteristic);
}

/**
 * Issue command to sensor to stop streaming accel and gyro data
 */
public void stopStreaming() {
    if (!mIsStreaming) {
        return;
    }
    //Disable notifications for combined
    BluetoothGattService huneoService = mBluetoothGatt.getService(UUID_HUNEO_SERVICE);
    BluetoothGattCharacteristic monitorCharacteristic = huneoService.getCharacteristic(UUID_HUNEO_MONITOR);
    monitorCharacteristic.setValue(0, FORMAT_UINT8, 0);
    mBluetoothGatt.writeCharacteristic(monitorCharacteristic);
}

/**
 * Trigger vibration haptics on the sensor
 */
public void triggerHaptics() {
    mServicesDiscovered.getTask().onSuccessTask(t -> {
        BluetoothGattService huneoService = mBluetoothGatt.getService(UUID_HUNEO_SERVICE);

        // Set vibration characteristic to 1
        BluetoothGattCharacteristic characteristic = huneoService.getCharacteristic(UUID_HUNEO_VIBRATOR);
        characteristic.setValue(0, FORMAT_UINT8, 0);
        mBluetoothGatt.writeCharacteristic(characteristic);

        return t;
    });
}

//Start Combined Notifications
private void startCombined() {
    mServicesDiscovered.getTask().onSuccessTask(t -> {
        BluetoothGattService huneoService = mBluetoothGatt.getService(UUID_HUNEO_SERVICE);

        // Set Combined characteristic to send notifications
        BluetoothGattCharacteristic combinedCharacteristic = huneoService.getCharacteristic(UUID_HUNEO_COMBINED);
        mBluetoothGatt.setCharacteristicNotification(combinedCharacteristic, true);

        //Set Combined Client Characteristic Config Descriptor to enable notifications
        BluetoothGattDescriptor combinedDescriptor = combinedCharacteristic.getDescriptor(UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
        combinedDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
        mBluetoothGatt.writeDescriptor(combinedDescriptor);

        return t;
    });
}

//Stop Combined notifications
private void stopCombined() {
    mServicesDiscovered.getTask().onSuccessTask(t -> {
        BluetoothGattService huneoService = mBluetoothGatt.getService(UUID_HUNEO_SERVICE);

        // Set Accelerometer characteristic to send notifications
        BluetoothGattCharacteristic combinedCharacteristic = huneoService.getCharacteristic(UUID_HUNEO_COMBINED);
        mBluetoothGatt.setCharacteristicNotification(combinedCharacteristic, false);

        // Set Accelerometer Client Characteristic Config Descriptor to enable notifications
        BluetoothGattDescriptor combinedDescriptor = combinedCharacteristic.getDescriptor(UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
        combinedDescriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
        mBluetoothGatt.writeDescriptor(combinedDescriptor);

        return t;
    });
}

// Convert two bytes into a short
private int unsignedShortToInt(byte firstByte, byte secondByte) {
    ByteBuffer bb = ByteBuffer.allocate(2);
    bb.order(ByteOrder.LITTLE_ENDIAN);
    bb.put(firstByte);
    bb.put(secondByte);
    short shortVal = bb.getShort(0);
    return shortVal >= 0 ? shortVal : 0x10000 + shortVal;
}

}

Hopefully, someone can help me out. I'm out of ideas.


Solution

  • Found the solution. The third-party forgot to mention that I had to set the right value to enable the characteristic. I tried that and it worked! Finally, got data values.