// (c) 2104 Don Coleman
//
// 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.megster.cordova.ble.central;
import android.app.Activity;
import android.bluetooth.*;
import android.os.Build;
import android.util.Base64;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.LOG;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* Peripheral wraps the BluetoothDevice and provides methods to convert to JSON.
*/
public class Peripheral extends BluetoothGattCallback {
// 0x2902 org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml
//public final static UUID CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB");
public final static UUID CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = UUIDHelper.uuidFromString("2902");
private static final String TAG = "Peripheral";
private BluetoothDevice device;
private byte[] advertisingData;
private int advertisingRSSI;
private boolean connected = false;
private boolean connecting = false;
private ConcurrentLinkedQueue<BLECommand> commandQueue = new ConcurrentLinkedQueue<BLECommand>();
private boolean bleProcessing;
BluetoothGatt gatt;
private CallbackContext connectCallback;
private CallbackContext readCallback;
private CallbackContext writeCallback;
private Map<String, CallbackContext> notificationCallbacks = new HashMap<String, CallbackContext>();
public Peripheral(BluetoothDevice device, int advertisingRSSI, byte[] scanRecord) {
this.device = device;
this.advertisingRSSI = advertisingRSSI;
this.advertisingData = scanRecord;
}
public void connect(CallbackContext callbackContext, Activity activity) {
BluetoothDevice device = getDevice();
connecting = true;
connectCallback = callbackContext;
if (Build.VERSION.SDK_INT < 23) {
gatt = device.connectGatt(activity, false, this);
} else {
gatt = device.connectGatt(activity, false, this, BluetoothDevice.TRANSPORT_LE);
}
PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);
}
public void disconnect() {
connectCallback = null;
connected = false;
connecting = false;
if (gatt != null) {
gatt.disconnect();
gatt.close();
gatt = null;
}
}
public JSONObject asJSONObject() {
JSONObject json = new JSONObject();
try {
json.put("name", device.getName());
json.put("id", device.getAddress()); // mac address
json.put("advertising", byteArrayToJSON(advertisingData));
// TODO real RSSI if we have it, else
json.put("rssi", advertisingRSSI);
} catch (JSONException e) { // this shouldn't happen
e.printStackTrace();
}
return json;
}
public JSONObject asJSONObject(String errorMessage) {
JSONObject json = new JSONObject();
try {
json.put("name", device.getName());
json.put("id", device.getAddress()); // mac address
json.put("errorMessage", errorMessage);
} catch (JSONException e) { // this shouldn't happen
e.printStackTrace();
}
return json;
}
public JSONObject asJSONObject(BluetoothGatt gatt) {
JSONObject json = asJSONObject();
try {
JSONArray servicesArray = new JSONArray();
JSONArray characteristicsArray = new JSONArray();
json.put("services", servicesArray);
json.put("characteristics", characteristicsArray);
if (connected && gatt != null) {
for (BluetoothGattService service : gatt.getServices()) {
servicesArray.put(UUIDHelper.uuidToString(service.getUuid()));
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
JSONObject characteristicsJSON = new JSONObject();
characteristicsArray.put(characteristicsJSON);
characteristicsJSON.put("service", UUIDHelper.uuidToString(service.getUuid()));
characteristicsJSON.put("characteristic", UUIDHelper.uuidToString(characteristic.getUuid()));
//characteristicsJSON.put("instanceId", characteristic.getInstanceId());
characteristicsJSON.put("properties", Helper.decodeProperties(characteristic));
// characteristicsJSON.put("propertiesValue", characteristic.getProperties());
if (characteristic.getPermissions() > 0) {
characteristicsJSON.put("permissions", Helper.decodePermissions(characteristic));
// characteristicsJSON.put("permissionsValue", characteristic.getPermissions());
}
JSONArray descriptorsArray = new JSONArray();
for (BluetoothGattDescriptor descriptor: characteristic.getDescriptors()) {
JSONObject descriptorJSON = new JSONObject();
descriptorJSON.put("uuid", UUIDHelper.uuidToString(descriptor.getUuid()));
descriptorJSON.put("value", descriptor.getValue()); // always blank
if (descriptor.getPermissions() > 0) {
descriptorJSON.put("permissions", Helper.decodePermissions(descriptor));
// descriptorJSON.put("permissionsValue", descriptor.getPermissions());
}
descriptorsArray.put(descriptorJSON);
}
if (descriptorsArray.length() > 0) {
characteristicsJSON.put("descriptors", descriptorsArray);
}
}
}
}
} catch (JSONException e) { // TODO better error handling
e.printStackTrace();
}
return json;
}
static JSONObject byteArrayToJSON(byte[] bytes) throws JSONException {
JSONObject object = new JSONObject();
object.put("CDVType", "ArrayBuffer");
object.put("data", Base64.encodeToString(bytes, Base64.NO_WRAP));
return object;
}
public boolean isConnected() {
return connected;
}
public boolean isConnecting() {
return connecting;
}
public BluetoothDevice getDevice() {
return device;
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
PluginResult result = new PluginResult(PluginResult.Status.OK, this.asJSONObject(gatt));
result.setKeepCallback(true);
connectCallback.sendPluginResult(result);
} else {
LOG.e(TAG, "Service discovery failed. status = " + status);
connectCallback.error(this.asJSONObject("Service discovery failed"));
disconnect();
}
}
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
this.gatt = gatt;
if (newState == BluetoothGatt.STATE_CONNECTED) {
connected = true;
connecting = false;
gatt.discoverServices();
} else {
if (connectCallback != null) {
connectCallback.error(this.asJSONObject("Peripheral Disconnected"));
}
disconnect();
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
LOG.d(TAG, "onCharacteristicChanged " + characteristic);
CallbackContext callback = notificationCallbacks.get(generateHashKey(characteristic));
if (callback != null) {
PluginResult result = new PluginResult(PluginResult.Status.OK, characteristic.getValue());
result.setKeepCallback(true);
callback.sendPluginResult(result);
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
LOG.d(TAG, "onCharacteristicRead " + characteristic);
if (readCallback != null) {
if (status == BluetoothGatt.GATT_SUCCESS) {
readCallback.success(characteristic.getValue());
} else {
readCallback.error("Error reading " + characteristic.getUuid() + " status=" + status);
}
readCallback = null;
}
commandCompleted();
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
LOG.d(TAG, "onCharacteristicWrite " + characteristic);
if (writeCallback != null) {
if (status == BluetoothGatt.GATT_SUCCESS) {
writeCallback.success();
} else {
writeCallback.error(status);
}
writeCallback = null;
}
commandCompleted();
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorWrite(gatt, descriptor, status);
LOG.d(TAG, "onDescriptorWrite " + descriptor);
commandCompleted();
}
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
super.onReadRemoteRssi(gatt, rssi, status);
if (readCallback != null) {
if (status == BluetoothGatt.GATT_SUCCESS) {
updateRssi(rssi);
readCallback.success(rssi);
} else {
readCallback.error("Error reading RSSI status=" + status);
}
readCallback = null;
}
commandCompleted();
}
// Update rssi and scanRecord.
public void update(int rssi, byte[] scanRecord) {
this.advertisingRSSI = rssi;
this.advertisingData = scanRecord;
}
public void updateRssi(int rssi) {
advertisingRSSI = rssi;
}
// This seems way too complicated
private void registerNotifyCallback(CallbackContext callbackContext, UUID serviceUUID, UUID characteristicUUID) {
boolean success = false;
if (gatt == null) {
callbackContext.error("BluetoothGatt is null");
return;
}
BluetoothGattService service = gatt.getService(serviceUUID);
BluetoothGattCharacteristic characteristic = findNotifyCharacteristic(service, characteristicUUID);
String key = generateHashKey(serviceUUID, characteristic);
if (characteristic != null) {
notificationCallbacks.put(key, callbackContext);
if (gatt.setCharacteristicNotification(characteristic, true)) {
// Why doesn't setCharacteristicNotification write the descriptor?
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIGURATION_UUID);
if (descriptor != null) {
// prefer notify over indicate
if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
} else if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
} else {
LOG.w(TAG, "Characteristic " + characteristicUUID + " does not have NOTIFY or INDICATE property set");
}
if (gatt.writeDescriptor(descriptor)) {
success = true;
} else {
callbackContext.error("Failed to set client characteristic notification for " + characteristicUUID);
}
} else {
callbackContext.error("Set notification failed for " + characteristicUUID);
}
} else {
callbackContext.error("Failed to register notification for " + characteristicUUID);
}
} else {
callbackContext.error("Characteristic " + characteristicUUID + " not found");
}
if (!success) {
commandCompleted();
}
}
private void removeNotifyCallback(CallbackContext callbackContext, UUID serviceUUID, UUID characteristicUUID) {
if (gatt == null) {
callbackContext.error("BluetoothGatt is null");
return;
}
BluetoothGattService service = gatt.getService(serviceUUID);
BluetoothGattCharacteristic characteristic = findNotifyCharacteristic(service, characteristicUUID);
String key = generateHashKey(serviceUUID, characteristic);
if (characteristic != null) {
notificationCallbacks.remove(key);
if (gatt.setCharacteristicNotification(characteristic, false)) {
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIGURATION_UUID);
if (descriptor != null) {
descriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);
}
callbackContext.success();
} else {
// TODO we can probably ignore and return success anyway since we removed the notification callback
callbackContext.error("Failed to stop notification for " + characteristicUUID);
}
} else {
callbackContext.error("Characteristic " + characteristicUUID + " not found");
}
commandCompleted();
}
// Some devices reuse UUIDs across characteristics, so we can't use service.getCharacteristic(characteristicUUID)
// instead check the UUID and properties for each characteristic in the service until we find the best match
// This function prefers Notify over Indicate
private BluetoothGattCharacteristic findNotifyCharacteristic(BluetoothGattService service, UUID characteristicUUID) {
BluetoothGattCharacteristic characteristic = null;
// Check for Notify first
List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
for (BluetoothGattCharacteristic c : characteristics) {
if ((c.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0 && characteristicUUID.equals(c.getUuid())) {
characteristic = c;
break;
}
}
if (characteristic != null) return characteristic;
// If there wasn't Notify Characteristic, check for Indicate
for (BluetoothGattCharacteristic c : characteristics) {
if ((c.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0 && characteristicUUID.equals(c.getUuid())) {
characteristic = c;
break;
}
}
// As a last resort, try and find ANY characteristic with this UUID, even if it doesn't have the correct properties
if (characteristic == null) {
characteristic = service.getCharacteristic(characteristicUUID);
}
return characteristic;
}
private void readCharacteristic(CallbackContext callbackContext, UUID serviceUUID, UUID characteristicUUID) {
boolean success = false;
if (gatt == null) {
callbackContext.error("BluetoothGatt is null");
return;
}
BluetoothGattService service = gatt.getService(serviceUUID);
BluetoothGattCharacteristic characteristic = findReadableCharacteristic(service, characteristicUUID);
if (characteristic == null) {
callbackContext.error("Characteristic " + characteristicUUID + " not found.");
} else {
readCallback = callbackContext;
if (gatt.readCharacteristic(characteristic)) {
success = true;
} else {
readCallback = null;
callbackContext.error("Read failed");
}
}
if (!success) {
commandCompleted();
}
}
private void readRSSI(CallbackContext callbackContext) {
boolean success = false;
if (gatt == null) {
callbackContext.error("BluetoothGatt is null");
return;
}
readCallback = callbackContext;
if (gatt.readRemoteRssi()) {
success = true;
} else {
readCallback = null;
callbackContext.error("Read RSSI failed");
}
if (!success) {
commandCompleted();
}
}
// Some peripherals re-use UUIDs for multiple characteristics so we need to check the properties
// and UUID of all characteristics instead of using service.getCharacteristic(characteristicUUID)
private BluetoothGattCharacteristic findReadableCharacteristic(BluetoothGattService service, UUID characteristicUUID) {
BluetoothGattCharacteristic characteristic = null;
int read = BluetoothGattCharacteristic.PROPERTY_READ;
List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
for (BluetoothGattCharacteristic c : characteristics) {
if ((c.getProperties() & read) != 0 && characteristicUUID.equals(c.getUuid())) {
characteristic = c;
break;
}
}
// As a last resort, try and find ANY characteristic with this UUID, even if it doesn't have the correct properties
if (characteristic == null) {
characteristic = service.getCharacteristic(characteristicUUID);
}
return characteristic;
}
private void writeCharacteristic(CallbackContext callbackContext, UUID serviceUUID, UUID characteristicUUID, byte[] data, int writeType) {
boolean success = false;
if (gatt == null) {
callbackContext.error("BluetoothGatt is null");
return;
}
BluetoothGattService service = gatt.getService(serviceUUID);
BluetoothGattCharacteristic characteristic = findWritableCharacteristic(service, characteristicUUID, writeType);
if (characteristic == null) {
callbackContext.error("Characteristic " + characteristicUUID + " not found.");
} else {
characteristic.setValue(data);
characteristic.setWriteType(writeType);
writeCallback = callbackContext;
if (gatt.writeCharacteristic(characteristic)) {
success = true;
} else {
writeCallback = null;
callbackContext.error("Write failed");
}
}
if (!success) {
commandCompleted();
}
}
// Some peripherals re-use UUIDs for multiple characteristics so we need to check the properties
// and UUID of all characteristics instead of using service.getCharacteristic(characteristicUUID)
private BluetoothGattCharacteristic findWritableCharacteristic(BluetoothGattService service, UUID characteristicUUID, int writeType) {
BluetoothGattCharacteristic characteristic = null;
// get write property
int writeProperty = BluetoothGattCharacteristic.PROPERTY_WRITE;
if (writeType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) {
writeProperty = BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE;
}
List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
for (BluetoothGattCharacteristic c : characteristics) {
if ((c.getProperties() & writeProperty) != 0 && characteristicUUID.equals(c.getUuid())) {
characteristic = c;
break;
}
}
// As a last resort, try and find ANY characteristic with this UUID, even if it doesn't have the correct properties
if (characteristic == null) {
characteristic = service.getCharacteristic(characteristicUUID);
}
return characteristic;
}
public void queueRead(CallbackContext callbackContext, UUID serviceUUID, UUID characteristicUUID) {
BLECommand command = new BLECommand(callbackContext, serviceUUID, characteristicUUID, BLECommand.READ);
queueCommand(command);
}
public void queueWrite(CallbackContext callbackContext, UUID serviceUUID, UUID characteristicUUID, byte[] data, int writeType) {
BLECommand command = new BLECommand(callbackContext, serviceUUID, characteristicUUID, data, writeType);
queueCommand(command);
}
public void queueRegisterNotifyCallback(CallbackContext callbackContext, UUID serviceUUID, UUID characteristicUUID) {
BLECommand command = new BLECommand(callbackContext, serviceUUID, characteristicUUID, BLECommand.REGISTER_NOTIFY);
queueCommand(command);
}
public void queueRemoveNotifyCallback(CallbackContext callbackContext, UUID serviceUUID, UUID characteristicUUID) {
BLECommand command = new BLECommand(callbackContext, serviceUUID, characteristicUUID, BLECommand.REMOVE_NOTIFY);
queueCommand(command);
}
public void queueReadRSSI(CallbackContext callbackContext) {
BLECommand command = new BLECommand(callbackContext, null, null, BLECommand.READ_RSSI);
queueCommand(command);
}
// add a new command to the queue
private void queueCommand(BLECommand command) {
LOG.d(TAG,"Queuing Command " + command);
commandQueue.add(command);
PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
result.setKeepCallback(true);
command.getCallbackContext().sendPluginResult(result);
if (!bleProcessing) {
processCommands();
}
}
// command finished, queue the next command
private void commandCompleted() {
LOG.d(TAG,"Processing Complete");
bleProcessing = false;
processCommands();
}
// process the queue
private void processCommands() {
LOG.d(TAG,"Processing Commands");
if (bleProcessing) { return; }
BLECommand command = commandQueue.poll();
if (command != null) {
if (command.getType() == BLECommand.READ) {
LOG.d(TAG,"Read " + command.getCharacteristicUUID());
bleProcessing = true;
readCharacteristic(command.getCallbackContext(), command.getServiceUUID(), command.getCharacteristicUUID());
} else if (command.getType() == BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) {
LOG.d(TAG,"Write " + command.getCharacteristicUUID());
bleProcessing = true;
writeCharacteristic(command.getCallbackContext(), command.getServiceUUID(), command.getCharacteristicUUID(), command.getData(), command.getType());
} else if (command.getType() == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) {
LOG.d(TAG,"Write No Response " + command.getCharacteristicUUID());
bleProcessing = true;
writeCharacteristic(command.getCallbackContext(), command.getServiceUUID(), command.getCharacteristicUUID(), command.getData(), command.getType());
} else if (command.getType() == BLECommand.REGISTER_NOTIFY) {
LOG.d(TAG,"Register Notify " + command.getCharacteristicUUID());
bleProcessing = true;
registerNotifyCallback(command.getCallbackContext(), command.getServiceUUID(), command.getCharacteristicUUID());
} else if (command.getType() == BLECommand.REMOVE_NOTIFY) {
LOG.d(TAG,"Remove Notify " + command.getCharacteristicUUID());
bleProcessing = true;
removeNotifyCallback(command.getCallbackContext(), command.getServiceUUID(), command.getCharacteristicUUID());
} else if (command.getType() == BLECommand.READ_RSSI) {
LOG.d(TAG,"Read RSSI");
bleProcessing = true;
readRSSI(command.getCallbackContext());
} else {
// this shouldn't happen
throw new RuntimeException("Unexpected BLE Command type " + command.getType());
}
} else {
LOG.d(TAG, "Command Queue is empty.");
}
}
private String generateHashKey(BluetoothGattCharacteristic characteristic) {
return generateHashKey(characteristic.getService().getUuid(), characteristic);
}
private String generateHashKey(UUID serviceUUID, BluetoothGattCharacteristic characteristic) {
return String.valueOf(serviceUUID) + "|" + characteristic.getUuid() + "|" + characteristic.getInstanceId();
}
}