/* * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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 software.amazon.freertos.amazonfreertossdk; 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.BluetoothProfile; import android.content.Context; import android.os.Handler; import android.os.HandlerThread; import android.util.Log; import software.amazon.freertos.amazonfreertossdk.deviceinfo.BrokerEndpoint; import software.amazon.freertos.amazonfreertossdk.deviceinfo.Mtu; import software.amazon.freertos.amazonfreertossdk.deviceinfo.Version; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.mobileconnectors.iot.AWSIotMqttClientStatusCallback; import com.amazonaws.mobileconnectors.iot.AWSIotMqttManager; import com.amazonaws.mobileconnectors.iot.AWSIotMqttMessageDeliveryCallback; import com.amazonaws.mobileconnectors.iot.AWSIotMqttNewMessageCallback; import com.amazonaws.mobileconnectors.iot.AWSIotMqttQos; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.KeyStore; import java.util.Arrays; import java.util.Formatter; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Queue; import java.util.UUID; import java.util.concurrent.Semaphore; import lombok.Getter; import lombok.NonNull; import software.amazon.freertos.amazonfreertossdk.mqttproxy.*; import software.amazon.freertos.amazonfreertossdk.networkconfig.*; import static android.bluetooth.BluetoothDevice.TRANSPORT_LE; import static software.amazon.freertos.amazonfreertossdk.AmazonFreeRTOSConstants.*; import static software.amazon.freertos.amazonfreertossdk.AmazonFreeRTOSConstants.AmazonFreeRTOSError.BLE_DISCONNECTED_ERROR; import static software.amazon.freertos.amazonfreertossdk.BleCommand.CommandType.DISCOVER_SERVICES; import static software.amazon.freertos.amazonfreertossdk.BleCommand.CommandType.NOTIFICATION; import static software.amazon.freertos.amazonfreertossdk.BleCommand.CommandType.READ_CHARACTERISTIC; import static software.amazon.freertos.amazonfreertossdk.BleCommand.CommandType.REQUEST_MTU; import static software.amazon.freertos.amazonfreertossdk.BleCommand.CommandType.WRITE_CHARACTERISTIC; import static software.amazon.freertos.amazonfreertossdk.BleCommand.CommandType.WRITE_DESCRIPTOR; public class AmazonFreeRTOSDevice { private static final String TAG = "FRD"; private static final boolean VDBG = false; private Context mContext; @Getter private BluetoothDevice mBluetoothDevice; private BluetoothGatt mBluetoothGatt; private BleConnectionStatusCallback mBleConnectionStatusCallback; private NetworkConfigCallback mNetworkConfigCallback; private DeviceInfoCallback mDeviceInfoCallback; private BleConnectionState mBleConnectionState = BleConnectionState.BLE_DISCONNECTED; private String mAmazonFreeRTOSLibVersion = "NA"; private String mAmazonFreeRTOSDeviceType = "NA"; private String mAmazonFreeRTOSDeviceId = "NA"; private boolean mGattAutoReconnect = false; private int mMtu = 0; private boolean rr = false; private Queue<BleCommand> mMqttQueue = new LinkedList<>(); private Queue<BleCommand> mNetworkQueue = new LinkedList<>(); private Queue<BleCommand> mIncomingQueue = new LinkedList<>(); private boolean mBleOperationInProgress = false; private boolean mRWinProgress = false; private Handler mHandler; private HandlerThread mHandlerThread; private byte[] mValueWritten; private Semaphore mutex = new Semaphore(1); private Semaphore mIncomingMutex = new Semaphore(1); //Buffer for receiving messages from device private ByteArrayOutputStream mTxLargeObject = new ByteArrayOutputStream(); private ByteArrayOutputStream mTxLargeNw = new ByteArrayOutputStream(); //Buffer for sending messages to device. private int mTotalPackets = 0; private int mPacketCount = 1; private int mMessageId = 0; private int mMaxPayloadLen = 0; private AWSIotMqttManager mIotMqttManager; private MqttConnectionState mMqttConnectionState = MqttConnectionState.MQTT_Disconnected; private AWSCredentialsProvider mAWSCredential; private KeyStore mKeystore; /** * Construct an AmazonFreeRTOSDevice instance. * * @param context The app context. Should be passed in by the app that creates a new instance * of AmazonFreeRTOSDevice. * @param device BluetoothDevice returned from BLE scan result. * @param cp AWS credential for connection to AWS IoT. If null is passed in, * then it will not be able to do MQTT proxy over BLE as it cannot * connect to AWS IoT. */ AmazonFreeRTOSDevice(@NonNull BluetoothDevice device, @NonNull Context context, AWSCredentialsProvider cp) { this(device, context, cp, null); } /** * Construct an AmazonFreeRTOSDevice instance. * * @param context The app context. Should be passed in by the app that creates a new instance * of AmazonFreeRTOSDevice. * @param device BluetoothDevice returned from BLE scan result. * @param ks the KeyStore which contains the certificate used to connect to AWS IoT. */ AmazonFreeRTOSDevice(@NonNull BluetoothDevice device, @NonNull Context context, KeyStore ks) { this(device, context, null, ks); } private AmazonFreeRTOSDevice(@NonNull BluetoothDevice device, @NonNull Context context, AWSCredentialsProvider cp, KeyStore ks) { mContext = context; mBluetoothDevice = device; mAWSCredential = cp; mKeystore = ks; } void connect(@NonNull final BleConnectionStatusCallback connectionStatusCallback, final boolean autoReconnect) { mBleConnectionStatusCallback = connectionStatusCallback; mHandlerThread = new HandlerThread("BleCommandHandler"); //TODO: unique thread name for each device? mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); mGattAutoReconnect = autoReconnect; mBluetoothGatt = mBluetoothDevice.connectGatt(mContext, autoReconnect, mGattCallback, TRANSPORT_LE); } private void cleanUp() { // If ble connection is lost, clear any pending ble command. mMqttQueue.clear(); mNetworkQueue.clear(); mIncomingQueue.clear(); mMessageId = 0; mMtu = 0; mMaxPayloadLen = 0; mTxLargeObject.reset(); mTotalPackets = 0; mPacketCount = 1; } /** * User initiated disconnect */ void disconnect() { if (mBluetoothGatt != null) { mGattAutoReconnect = false; mBluetoothGatt.disconnect(); } } /** * Sends a ListNetworkReq command to the connected BLE device. The available WiFi networks found * by the connected BLE device will be returned in the callback as a ListNetworkResp. Each found * WiFi network should trigger the callback once. For example, if there are 10 available networks * found by the BLE device, this callback will be triggered 10 times, each containing one * ListNetworkResp that represents that WiFi network. In addition, the order of the callbacks will * be triggered as follows: the saved networks will be returned first, in decreasing order of their * preference, as denoted by their index. (The smallest non-negative index denotes the highest * preference, and is therefore returned first.) For example, the saved network with index 0 will * be returned first, then the saved network with index 1, then index 2, etc. After all saved * networks have been returned, the non-saved networks will be returned, in the decreasing order * of their RSSI value, a network with higher RSSI value will be returned before one with lower * RSSI value. * * @param listNetworkReq The ListNetwork request * @param callback The callback which will be triggered once the BLE device sends a ListNetwork * response. */ public void listNetworks(ListNetworkReq listNetworkReq, NetworkConfigCallback callback) { mNetworkConfigCallback = callback; byte[] listNetworkReqBytes = listNetworkReq.encode(); sendDataToDevice(UUID_NETWORK_SERVICE, UUID_NETWORK_RX, UUID_NETWORK_RXLARGE, listNetworkReqBytes); } /** * Sends a SaveNetworkReq command to the connected BLE device. The SaveNetworkReq contains the * network credential. A SaveNetworkResp will be sent by the BLE device and triggers the callback. * To get the updated order of all networks, call listNetworks again. * * @param saveNetworkReq The SaveNetwork request. * @param callback The callback that is triggered once the BLE device sends a SaveNetwork response. */ public void saveNetwork(SaveNetworkReq saveNetworkReq, NetworkConfigCallback callback) { mNetworkConfigCallback = callback; byte[] saveNetworkReqBytes = saveNetworkReq.encode(); sendDataToDevice(UUID_NETWORK_SERVICE, UUID_NETWORK_RX, UUID_NETWORK_RXLARGE, saveNetworkReqBytes); } /** * Sends an EditNetworkReq command to the connected BLE device. The EditNetwork request is used * to update the preference of a saved network. It contains the current index of the saved network * to be updated, and the desired new index of the save network to be updated to. Both the current * index and the new index must be one of those saved networks. Behavior is undefined if an index * of an unsaved network is provided in the EditNetworkReq. * To get the updated order of all networks, call listNetworks again. * * @param editNetworkReq The EditNetwork request. * @param callback The callback that is triggered once the BLE device sends an EditNetwork response. */ public void editNetwork(EditNetworkReq editNetworkReq, NetworkConfigCallback callback) { mNetworkConfigCallback = callback; byte[] editNetworkReqBytes = editNetworkReq.encode(); sendDataToDevice(UUID_NETWORK_SERVICE, UUID_NETWORK_RX, UUID_NETWORK_RXLARGE, editNetworkReqBytes); } /** * Sends a DeleteNetworkReq command to the connected BLE device. The saved network with the index * specified in the delete network request will be deleted, making it a non-saved network again. * To get the updated order of all networks, call listNetworks again. * * @param deleteNetworkReq The DeleteNetwork request. * @param callback The callback that is triggered once the BLE device sends a DeleteNetwork response. */ public void deleteNetwork(DeleteNetworkReq deleteNetworkReq, NetworkConfigCallback callback) { mNetworkConfigCallback = callback; byte[] deleteNetworkReqBytes = deleteNetworkReq.encode(); sendDataToDevice(UUID_NETWORK_SERVICE, UUID_NETWORK_RX, UUID_NETWORK_RXLARGE, deleteNetworkReqBytes); } /** * Get the current mtu value between device and Android phone. This method returns immediately. * The request to get mtu value is asynchronous through BLE command. The response will be delivered * through DeviceInfoCallback. * * @param callback The callback to notify app of current mtu value. */ public void getMtu(DeviceInfoCallback callback) { mDeviceInfoCallback = callback; if (!getMtu() && mDeviceInfoCallback != null) { mDeviceInfoCallback.onError(BLE_DISCONNECTED_ERROR); } } /** * Get the current broker endpoint on the device. This broker endpoint is used to connect to AWS * IoT, hence, this is also the AWS IoT endpoint. This method returns immediately. * The request is sent asynchronously through BLE command. The response will be delivered * through DeviceInfoCallback. * * @param callback The callback to notify app of current broker endpoint on device. */ public void getBrokerEndpoint(DeviceInfoCallback callback) { mDeviceInfoCallback = callback; if (!getBrokerEndpoint() && mDeviceInfoCallback != null) { mDeviceInfoCallback.onError(BLE_DISCONNECTED_ERROR); } } /** * Get the AmazonFreeRTOS library software version running on the device. This method returns * immediately. The request is sent asynchronously through BLE command. The response will be * delivered through DeviceInfoCallback. * * @param callback The callback to notify app of current software version. */ public void getDeviceVersion(DeviceInfoCallback callback) { mDeviceInfoCallback = callback; if (!getDeviceVersion() && mDeviceInfoCallback != null) { mDeviceInfoCallback.onError(BLE_DISCONNECTED_ERROR); } } /** * Try to read a characteristic from the Gatt service. If pairing is enabled, it will be triggered * by this action. */ private void probe() { getDeviceVersion(); } /** * Initialize the Gatt services */ private void initialize() { getDeviceType(); getDeviceId(); getMtu(); sendBleCommand(new BleCommand(WRITE_DESCRIPTOR, UUID_MQTT_PROXY_TX, UUID_MQTT_PROXY_SERVICE)); sendBleCommand(new BleCommand(WRITE_DESCRIPTOR, UUID_MQTT_PROXY_TXLARGE, UUID_MQTT_PROXY_SERVICE)); sendBleCommand(new BleCommand(WRITE_DESCRIPTOR, UUID_NETWORK_TX, UUID_NETWORK_SERVICE)); sendBleCommand(new BleCommand(WRITE_DESCRIPTOR, UUID_NETWORK_TXLARGE, UUID_NETWORK_SERVICE)); } private void enableService(final String serviceUuid, final boolean enable) { byte[] ready = new byte[1]; if (enable) { ready[0] = 1; } else { ready[0] = 0; } switch (serviceUuid) { case UUID_NETWORK_SERVICE: Log.i(TAG, (enable ? "Enabling" : "Disabling") + " Wifi provisioning"); sendBleCommand(new BleCommand(WRITE_CHARACTERISTIC, UUID_NETWORK_CONTROL, UUID_NETWORK_SERVICE, ready)); break; case UUID_MQTT_PROXY_SERVICE: if (mKeystore != null || mAWSCredential != null) { Log.i(TAG, (enable ? "Enabling" : "Disabling") + " MQTT Proxy"); sendBleCommand(new BleCommand(WRITE_CHARACTERISTIC, UUID_MQTT_PROXY_CONTROL, UUID_MQTT_PROXY_SERVICE, ready)); } break; default: Log.w(TAG, "Unknown service. Ignoring."); } } private void processIncomingQueue() { try { mIncomingMutex.acquire(); while (mIncomingQueue.size() != 0) { BleCommand bleCommand = mIncomingQueue.poll(); Log.d(TAG, "Processing incoming queue. size: " + mIncomingQueue.size()); byte[] responseBytes = bleCommand.getData(); String cUuid = bleCommand.getCharacteristicUuid(); switch (cUuid) { case UUID_MQTT_PROXY_TX: handleMqttTxMessage(responseBytes); break; case UUID_MQTT_PROXY_TXLARGE: try { mTxLargeObject.write(responseBytes); sendBleCommand(new BleCommand(READ_CHARACTERISTIC, UUID_MQTT_PROXY_TXLARGE, UUID_MQTT_PROXY_SERVICE)); } catch (IOException e) { Log.e(TAG, "Failed to concatenate byte array.", e); } break; case UUID_NETWORK_TX: handleNwTxMessage(responseBytes); break; case UUID_NETWORK_TXLARGE: try { mTxLargeNw.write(responseBytes); sendBleCommand(new BleCommand(READ_CHARACTERISTIC, UUID_NETWORK_TXLARGE, UUID_NETWORK_SERVICE)); } catch (IOException e) { Log.e(TAG, "Failed to concatenate byte array.", e); } break; default: Log.e(TAG, "Unknown characteristic " + cUuid); } } mIncomingMutex.release(); } catch (InterruptedException e) { Log.e(TAG, "Incoming mutex error, ", e); } } /** * This is the callback for all BLE commands sent from SDK to device. The response of BLE * command is included in the callback, together with the status code. */ private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { Log.i(TAG, "BLE connection state changed: " + status + "; new state: " + AmazonFreeRTOSConstants.BleConnectionState.values()[newState]); if (status == BluetoothGatt.GATT_SUCCESS) { if (newState == BluetoothProfile.STATE_CONNECTED) { int bondState = mBluetoothDevice.getBondState(); mBleConnectionState = AmazonFreeRTOSConstants.BleConnectionState.BLE_CONNECTED; Log.i(TAG, "Connected to GATT server."); // If the device is already bonded or will not bond we can call discoverServices() immediately if (bondState == BluetoothDevice.BOND_NONE || bondState == BluetoothDevice.BOND_BONDED) { discoverServices(); } } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { mBleConnectionState = AmazonFreeRTOSConstants.BleConnectionState.BLE_DISCONNECTED; Log.i(TAG, "Disconnected from GATT server."); // If ble connection is closed, there's no need to keep mqtt connection open. if (mMqttConnectionState != AmazonFreeRTOSConstants.MqttConnectionState.MQTT_Disconnected) { disconnectFromIot(); } // If not using auto reconnect or if user initiated disconnect if (!mGattAutoReconnect) { gatt.close(); mBluetoothGatt = null; } cleanUp(); } } else { Log.i(TAG, "Disconnected from GATT server due to error ot peripheral initiated disconnect."); mBleConnectionState = AmazonFreeRTOSConstants.BleConnectionState.BLE_DISCONNECTED; if (mMqttConnectionState != AmazonFreeRTOSConstants.MqttConnectionState.MQTT_Disconnected) { disconnectFromIot(); } if (!mGattAutoReconnect) { gatt.close(); mBluetoothGatt = null; } cleanUp(); } mBleConnectionStatusCallback.onBleConnectionStatusChanged(mBleConnectionState); } @Override // New services discovered public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { Log.i(TAG, "Discovered Ble gatt services successfully. Bonding state: " + mBluetoothDevice.getBondState()); describeGattServices(mBluetoothGatt.getServices()); if (mBluetoothDevice.getBondState() != BluetoothDevice.BOND_BONDING) { probe(); } } else { Log.e(TAG, "onServicesDiscovered received: " + status); disconnect(); } processNextBleCommand(); } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { byte[] responseBytes = characteristic.getValue(); Log.d(TAG, "->->-> Characteristic changed for: " + uuidToName.get(characteristic.getUuid().toString()) + " with data: " + bytesToHexString(responseBytes)); BleCommand incomingCommand = new BleCommand(NOTIFICATION, characteristic.getUuid().toString(), characteristic.getService().getUuid().toString(), responseBytes); mIncomingQueue.add(incomingCommand); if (!mRWinProgress) { processIncomingQueue(); } } @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { Log.d(TAG, "onDescriptorWrite for characteristic: " + uuidToName.get(descriptor.getCharacteristic().getUuid().toString()) + "; Status: " + (status == 0 ? "Success" : status)); processNextBleCommand(); } @Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { Log.i(TAG, "onMTUChanged : " + mtu + " status: " + (status == 0 ? "Success" : status)); mMtu = mtu; mMaxPayloadLen = Math.max(mMtu - 3, 0); // The BLE service should be initialized at this stage if (mBleConnectionState == BleConnectionState.BLE_INITIALIZING) { mBleConnectionState = AmazonFreeRTOSConstants.BleConnectionState.BLE_INITIALIZED; mBleConnectionStatusCallback.onBleConnectionStatusChanged(mBleConnectionState); } enableService(UUID_NETWORK_SERVICE, true); enableService(UUID_MQTT_PROXY_SERVICE, true); processNextBleCommand(); } @Override // Result of a characteristic read operation public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { mRWinProgress = false; Log.d(TAG, "->->-> onCharacteristicRead status: " + (status == 0 ? "Success. " : status)); if (status == BluetoothGatt.GATT_SUCCESS) { // On the first successful read we enable the services if (mBleConnectionState == BleConnectionState.BLE_CONNECTED) { Log.d(TAG, "GATT services initializing..."); mBleConnectionState = AmazonFreeRTOSConstants.BleConnectionState.BLE_INITIALIZING; mBleConnectionStatusCallback.onBleConnectionStatusChanged(mBleConnectionState); initialize(); } byte[] responseBytes = characteristic.getValue(); Log.d(TAG, "->->-> onCharacteristicRead: " + bytesToHexString(responseBytes)); switch (characteristic.getUuid().toString()) { case UUID_MQTT_PROXY_TXLARGE: try { mTxLargeObject.write(responseBytes); if (responseBytes.length < mMaxPayloadLen) { byte[] largeMessage = mTxLargeObject.toByteArray(); Log.d(TAG, "MQTT Large object received from device successfully: " + bytesToHexString(largeMessage)); handleMqttTxMessage(largeMessage); mTxLargeObject.reset(); } else { sendBleCommand(new BleCommand(READ_CHARACTERISTIC, UUID_MQTT_PROXY_TXLARGE, UUID_MQTT_PROXY_SERVICE)); } } catch (IOException e) { Log.e(TAG, "Failed to concatenate byte array.", e); } break; case UUID_NETWORK_TXLARGE: try { mTxLargeNw.write(responseBytes); if (responseBytes.length < mMaxPayloadLen) { byte[] largeMessage = mTxLargeNw.toByteArray(); Log.d(TAG, "NW Large object received from device successfully: " + bytesToHexString(largeMessage)); handleNwTxMessage(largeMessage); mTxLargeNw.reset(); } else { sendBleCommand(new BleCommand(READ_CHARACTERISTIC, UUID_NETWORK_TXLARGE, UUID_NETWORK_SERVICE)); } } catch (IOException e) { Log.e(TAG, "Failed to concatenate byte array.", e); } break; case UUID_DEVICE_MTU: Mtu currentMtu = new Mtu(); currentMtu.mtu = new String(responseBytes); Log.i(TAG, "Default MTU is set to: " + currentMtu.mtu); try { mMtu = Integer.parseInt(currentMtu.mtu); mMaxPayloadLen = Math.max(mMtu - 3, 0); if (mDeviceInfoCallback != null) { mDeviceInfoCallback.onObtainMtu(mMtu); } setMtu(mMtu); } catch (NumberFormatException e) { Log.e(TAG, "Cannot parse default MTU value."); } break; case UUID_IOT_ENDPOINT: BrokerEndpoint currentEndpoint = new BrokerEndpoint(); currentEndpoint.brokerEndpoint = new String(responseBytes); Log.i(TAG, "Current broker endpoint is set to: " + currentEndpoint.brokerEndpoint); if (mDeviceInfoCallback != null) { mDeviceInfoCallback.onObtainBrokerEndpoint(currentEndpoint.brokerEndpoint); } break; case UUID_DEVICE_VERSION: Version currentVersion = new Version(); currentVersion.version = new String(responseBytes); if (!currentVersion.version.isEmpty()) { mAmazonFreeRTOSLibVersion = currentVersion.version; } Log.i(TAG, "Ble software version on device is: " + currentVersion.version); if (mDeviceInfoCallback != null) { mDeviceInfoCallback.onObtainDeviceSoftwareVersion(currentVersion.version); } break; case UUID_DEVICE_PLATFORM: String platform = new String(responseBytes); if (!platform.isEmpty()) { mAmazonFreeRTOSDeviceType = platform; } Log.i(TAG, "Device type is: " + mAmazonFreeRTOSDeviceType); break; case UUID_DEVICE_ID: String devId = new String(responseBytes); if (!devId.isEmpty()) { mAmazonFreeRTOSDeviceId = devId; } Log.i(TAG, "Device id is: " + mAmazonFreeRTOSDeviceId); break; default: Log.w(TAG, "Unknown characteristic read. "); } } processIncomingQueue(); processNextBleCommand(); } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { mRWinProgress = false; byte[] value = characteristic.getValue(); Log.d(TAG, "onCharacteristicWrite for: " + uuidToName.get(characteristic.getUuid().toString()) + "; status: " + (status == 0 ? "Success" : status) + "; value: " + bytesToHexString(value)); if (Arrays.equals(mValueWritten, value)) { processIncomingQueue(); processNextBleCommand(); } else { Log.e(TAG, "values don't match!"); } } }; /** * Handle mqtt messages received from device. * * @param message message received from device. */ private void handleMqttTxMessage(byte[] message) { MessageType messageType = new MessageType(); if (!messageType.decode(message)) { return; } Log.i(TAG, "Handling Mqtt Message type : " + messageType.type); switch (messageType.type) { case MQTT_MSG_CONNECT: final Connect connect = new Connect(); if (connect.decode(message)) { connectToIoT(connect); } break; case MQTT_MSG_SUBSCRIBE: final Subscribe subscribe = new Subscribe(); if (subscribe.decode(message)) { Log.d(TAG, subscribe.toString()); subscribeToIoT(subscribe); /* Currently, because the IoT part of aws mobile sdk for Android does not provide suback callback when subscribe is successful, we create a fake suback message and send to device as a workaround. Wait for 0.5 sec so that the subscribe is complete. Potential bug: Message is received from the subscribed topic before suback is sent to device. */ mHandler.postDelayed(new Runnable() { @Override public void run() { sendSubAck(subscribe); } }, 500); } break; case MQTT_MSG_UNSUBSCRIBE: final Unsubscribe unsubscribe = new Unsubscribe(); if (unsubscribe.decode(message)) { unsubscribeToIoT(unsubscribe); /* TODO: add unsuback support in Aws Mobile sdk */ sendUnsubAck(unsubscribe); } break; case MQTT_MSG_PUBLISH: final Publish publish = new Publish(); if (publish.decode(message)) { mMessageId = publish.getMsgID(); publishToIoT(publish); } break; case MQTT_MSG_DISCONNECT: disconnectFromIot(); break; case MQTT_MSG_PUBACK: /* AWS Iot SDK currently sends pub ack back to cloud without waiting for pub ack from device. */ final Puback puback = new Puback(); if (puback.decode(message)) { Log.w(TAG, "Received mqtt pub ack from device. MsgID: " + puback.msgID); } break; case MQTT_MSG_PINGREQ: PingResp pingResp = new PingResp(); byte[] pingRespBytes = pingResp.encode(); sendDataToDevice(UUID_MQTT_PROXY_SERVICE, UUID_MQTT_PROXY_RX, UUID_MQTT_PROXY_RXLARGE, pingRespBytes); break; default: Log.e(TAG, "Unknown mqtt message type: " + messageType.type); } } private void handleNwTxMessage(byte[] message) { MessageType messageType = new MessageType(); if (!messageType.decode(message)) { return; } Log.i(TAG, "Handling Network Message type : " + messageType.type); switch (messageType.type) { case LIST_NETWORK_RESP: ListNetworkResp listNetworkResp = new ListNetworkResp(); if (listNetworkResp.decode(message) && mNetworkConfigCallback != null) { Log.d(TAG, listNetworkResp.toString()); mNetworkConfigCallback.onListNetworkResponse(listNetworkResp); } break; case SAVE_NETWORK_RESP: SaveNetworkResp saveNetworkResp = new SaveNetworkResp(); if (saveNetworkResp.decode(message) && mNetworkConfigCallback != null) { mNetworkConfigCallback.onSaveNetworkResponse(saveNetworkResp); } break; case EDIT_NETWORK_RESP: EditNetworkResp editNetworkResp = new EditNetworkResp(); if (editNetworkResp.decode(message) && mNetworkConfigCallback != null) { mNetworkConfigCallback.onEditNetworkResponse(editNetworkResp); } break; case DELETE_NETWORK_RESP: DeleteNetworkResp deleteNetworkResp = new DeleteNetworkResp(); if (deleteNetworkResp.decode(message) && mNetworkConfigCallback != null) { mNetworkConfigCallback.onDeleteNetworkResponse(deleteNetworkResp); } break; default: Log.e(TAG, "Unknown network message type: " + messageType.type); } } private void connectToIoT(final Connect connect) { if (mMqttConnectionState == AmazonFreeRTOSConstants.MqttConnectionState.MQTT_Connected) { Log.w(TAG, "Already connected to IOT, sending connack to device again."); sendConnAck(); return; } if (mMqttConnectionState != AmazonFreeRTOSConstants.MqttConnectionState.MQTT_Disconnected) { Log.w(TAG, "Previous connection is active, please retry or disconnect mqtt first."); return; } mIotMqttManager = new AWSIotMqttManager(connect.clientID, connect.brokerEndpoint); Map<String, String> userMetaData = new HashMap<>(); userMetaData.put("AFRSDK", "Android"); //userMetaData.put("AFRSDKVersion", AMAZONFREERTOS_SDK_VERSION); userMetaData.put("AFRLibVersion", mAmazonFreeRTOSLibVersion); userMetaData.put("Platform", mAmazonFreeRTOSDeviceType); userMetaData.put("AFRDevID", mAmazonFreeRTOSDeviceId); mIotMqttManager.updateUserMetaData(userMetaData); AWSIotMqttClientStatusCallback mqttClientStatusCallback = new AWSIotMqttClientStatusCallback() { @Override public void onStatusChanged(AWSIotMqttClientStatus status, Throwable throwable) { Log.i(TAG, "mqtt connection status changed to: " + status); switch (status) { case Connected: mMqttConnectionState = AmazonFreeRTOSConstants.MqttConnectionState.MQTT_Connected; //sending connack if (isBLEConnected() && mBluetoothGatt != null) { sendConnAck(); } else { Log.e(TAG, "Cannot send CONNACK because BLE connection is: " + mBleConnectionState); } break; case Connecting: case Reconnecting: mMqttConnectionState = AmazonFreeRTOSConstants.MqttConnectionState.MQTT_Connecting; break; case ConnectionLost: mMqttConnectionState = AmazonFreeRTOSConstants.MqttConnectionState.MQTT_Disconnected; break; default: Log.e(TAG, "Unknown mqtt connection state: " + status); } } }; if (mKeystore != null) { Log.i(TAG, "Connecting to IoT using KeyStore: " + connect.brokerEndpoint); mIotMqttManager.connect(mKeystore, mqttClientStatusCallback); } else { Log.i(TAG, "Connecting to IoT using AWS credential: " + connect.brokerEndpoint); mIotMqttManager.connect(mAWSCredential, mqttClientStatusCallback); } } private void disconnectFromIot() { if (mIotMqttManager != null) { try { mIotMqttManager.disconnect(); mMqttConnectionState = AmazonFreeRTOSConstants.MqttConnectionState.MQTT_Disconnected; } catch (Exception e) { Log.e(TAG, "Mqtt disconnect error: ", e); } } } private void subscribeToIoT(final Subscribe subscribe) { if (mMqttConnectionState != AmazonFreeRTOSConstants.MqttConnectionState.MQTT_Connected) { Log.e(TAG, "Cannot subscribe because mqtt state is not connected."); return; } for (int i = 0; i < subscribe.topics.size(); i++) { try { String topic = subscribe.topics.get(i); Log.i(TAG, "Subscribing to IoT on topic : " + topic); final int QoS = subscribe.qoSs.get(i); AWSIotMqttQos qos = (QoS == 0 ? AWSIotMqttQos.QOS0 : AWSIotMqttQos.QOS1); mIotMqttManager.subscribeToTopic(topic, qos, new AWSIotMqttNewMessageCallback() { @Override public void onMessageArrived(final String topic, final byte[] data) { String message = new String(data, StandardCharsets.UTF_8); Log.i(TAG, " Message arrived on topic: " + topic); Log.v(TAG, " Message: " + message); Publish publish = new Publish( MQTT_MSG_PUBLISH, topic, mMessageId, QoS, data ); publishToDevice(publish); } }); } catch (Exception e) { Log.e(TAG, "Subscription error.", e); } } } private void unsubscribeToIoT(final Unsubscribe unsubscribe) { if (mMqttConnectionState != AmazonFreeRTOSConstants.MqttConnectionState.MQTT_Connected) { Log.e(TAG, "Cannot unsubscribe because mqtt state is not connected."); return; } for (int i = 0; i < unsubscribe.topics.size(); i++) { try { String topic = unsubscribe.topics.get(i); Log.i(TAG, "UnSubscribing to IoT on topic : " + topic); mIotMqttManager.unsubscribeTopic(topic); } catch (Exception e) { Log.e(TAG, "Unsubscribe error.", e); } } } private void publishToIoT(final Publish publish) { if (mMqttConnectionState != AmazonFreeRTOSConstants.MqttConnectionState.MQTT_Connected) { Log.e(TAG, "Cannot publish message to IoT because mqtt connection state is not connected."); return; } AWSIotMqttMessageDeliveryCallback deliveryCallback = new AWSIotMqttMessageDeliveryCallback() { @Override public void statusChanged(MessageDeliveryStatus messageDeliveryStatus, Object o) { Log.d(TAG, "Publish msg delivery status: " + messageDeliveryStatus.toString()); if (messageDeliveryStatus == MessageDeliveryStatus.Success && publish.getQos() == 1) { sendPubAck(publish); } } }; try { String topic = publish.getTopic(); byte[] data = publish.getPayload(); Log.i(TAG, "Sending mqtt message to IoT on topic: " + topic + " message: " + new String(data) + " MsgID: " + publish.getMsgID()); mIotMqttManager.publishData(data, topic, AWSIotMqttQos.values()[publish.getQos()], deliveryCallback, null); } catch (Exception e) { Log.e(TAG, "Publish error.", e); } } private void sendConnAck() { Connack connack = new Connack(); connack.type = MQTT_MSG_CONNACK; connack.status = AmazonFreeRTOSConstants.MqttConnectionState.MQTT_Connected.ordinal(); byte[] connackBytes = connack.encode(); sendDataToDevice(UUID_MQTT_PROXY_SERVICE, UUID_MQTT_PROXY_RX, UUID_MQTT_PROXY_RXLARGE, connackBytes); } private boolean isBLEConnected() { return mBleConnectionState == BleConnectionState.BLE_CONNECTED || mBleConnectionState == BleConnectionState.BLE_INITIALIZED || mBleConnectionState == BleConnectionState.BLE_INITIALIZING; } private void sendSubAck(final Subscribe subscribe) { if (!isBLEConnected() && mBluetoothGatt != null) { Log.e(TAG, "Cannot send SUB ACK to BLE device because BLE connection state" + " is not connected"); return; } Log.i(TAG, "Sending SUB ACK back to device."); Suback suback = new Suback(); suback.type = MQTT_MSG_SUBACK; suback.msgID = subscribe.msgID; suback.status = subscribe.qoSs.get(0); byte[] subackBytes = suback.encode(); sendDataToDevice(UUID_MQTT_PROXY_SERVICE, UUID_MQTT_PROXY_RX, UUID_MQTT_PROXY_RXLARGE, subackBytes); } private void sendUnsubAck(final Unsubscribe unsubscribe) { if (!isBLEConnected() && mBluetoothGatt != null) { Log.e(TAG, "Cannot send Unsub ACK to BLE device because BLE connection state" + " is not connected"); return; } Log.i(TAG, "Sending Unsub ACK back to device."); Unsuback unsuback = new Unsuback(); unsuback.type = MQTT_MSG_UNSUBACK; unsuback.msgID = unsubscribe.msgID; byte[] unsubackBytes = unsuback.encode(); sendDataToDevice(UUID_MQTT_PROXY_SERVICE, UUID_MQTT_PROXY_RX, UUID_MQTT_PROXY_RXLARGE, unsubackBytes); } private void sendPubAck(final Publish publish) { if (!isBLEConnected() && mBluetoothGatt != null) { Log.e(TAG, "Cannot send PUB ACK to BLE device because BLE connection state" + " is not connected"); return; } Log.i(TAG, "Sending PUB ACK back to device. MsgID: " + publish.getMsgID()); Puback puback = new Puback(); puback.type = MQTT_MSG_PUBACK; puback.msgID = publish.getMsgID(); byte[] pubackBytes = puback.encode(); sendDataToDevice(UUID_MQTT_PROXY_SERVICE, UUID_MQTT_PROXY_RX, UUID_MQTT_PROXY_RXLARGE, pubackBytes); } private void publishToDevice(final Publish publish) { if (!isBLEConnected() && mBluetoothGatt != null) { Log.e(TAG, "Cannot deliver mqtt message to BLE device because BLE connection state" + " is not connected"); return; } Log.d(TAG, "Sending received mqtt message back to device, topic: " + publish.getTopic() + " payload bytes: " + bytesToHexString(publish.getPayload()) + " MsgID: " + publish.getMsgID()); byte[] publishBytes = publish.encode(); sendDataToDevice(UUID_MQTT_PROXY_SERVICE, UUID_MQTT_PROXY_RX, UUID_MQTT_PROXY_RXLARGE, publishBytes); } private void discoverServices() { if (isBLEConnected() && mBluetoothGatt != null) { sendBleCommand(new BleCommand(DISCOVER_SERVICES)); } else { Log.w(TAG, "Bluetooth connection state is not connected."); } } private void setMtu(int mtu) { if (isBLEConnected() && mBluetoothGatt != null) { Log.i(TAG, "Setting mtu to: " + mtu); sendBleCommand(new BleCommand(REQUEST_MTU, mtu)); } else { Log.w(TAG, "Bluetooth connection state is not connected."); } } private boolean getMtu() { if (isBLEConnected() && mBluetoothGatt != null) { Log.d(TAG, "Getting current MTU."); sendBleCommand(new BleCommand(READ_CHARACTERISTIC, UUID_DEVICE_MTU, UUID_DEVICE_INFORMATION_SERVICE)); return true; } else { Log.w(TAG, "Bluetooth is not connected."); return false; } } private boolean getBrokerEndpoint() { if (isBLEConnected() && mBluetoothGatt != null) { Log.d(TAG, "Getting broker endpoint."); sendBleCommand(new BleCommand(READ_CHARACTERISTIC, UUID_IOT_ENDPOINT, UUID_DEVICE_INFORMATION_SERVICE)); return true; } else { Log.w(TAG, "Bluetooth is not connected."); return false; } } private boolean getDeviceVersion() { if (isBLEConnected() && mBluetoothGatt != null) { Log.d(TAG, "Getting ble software version on device."); sendBleCommand(new BleCommand(READ_CHARACTERISTIC, UUID_DEVICE_VERSION, UUID_DEVICE_INFORMATION_SERVICE)); return true; } else { Log.w(TAG, "Bluetooth is not connected."); return false; } } private boolean getDeviceType() { if (isBLEConnected() && mBluetoothGatt != null) { Log.d(TAG, "Getting device type..."); sendBleCommand(new BleCommand(READ_CHARACTERISTIC, UUID_DEVICE_PLATFORM, UUID_DEVICE_INFORMATION_SERVICE)); return true; } else { Log.w(TAG, "Bluetooth is not connected."); return false; } } private boolean getDeviceId() { if (isBLEConnected() && mBluetoothGatt != null) { Log.d(TAG, "Getting device cert id..."); sendBleCommand(new BleCommand(READ_CHARACTERISTIC, UUID_DEVICE_ID, UUID_DEVICE_INFORMATION_SERVICE)); return true; } else { Log.w(TAG, "Bluetooth is not connected."); return false; } } private void sendDataToDevice(final String service, final String rx, final String rxlarge, byte[] data) { if (data != null) { if (data.length < mMaxPayloadLen) { sendBleCommand(new BleCommand(WRITE_CHARACTERISTIC, rx, service, data)); } else { mTotalPackets = data.length / mMaxPayloadLen + 1; Log.i(TAG, "This message is larger than max payload size: " + mMaxPayloadLen + ". Breaking down to " + mTotalPackets + " packets."); mPacketCount = 0; //reset packet count while (mMaxPayloadLen * mPacketCount <= data.length) { byte[] packet = Arrays.copyOfRange(data, mMaxPayloadLen * mPacketCount, Math.min(data.length, mMaxPayloadLen * mPacketCount + mMaxPayloadLen)); mPacketCount++; Log.d(TAG, "Packet #" + mPacketCount + ": " + bytesToHexString(packet)); sendBleCommand(new BleCommand(WRITE_CHARACTERISTIC, rxlarge, service, packet)); } } } } private void sendBleCommand(final BleCommand command) { if (UUID_MQTT_PROXY_SERVICE.equals(command.getServiceUuid())) { mMqttQueue.add(command); } else { mNetworkQueue.add(command); } processBleCommandQueue(); } private void processBleCommandQueue() { try { mutex.acquire(); if (mBleOperationInProgress) { Log.d(TAG, "Ble operation is in progress. mqtt queue: " + mMqttQueue.size() + " network queue: " + mNetworkQueue.size()); } else { if (mMqttQueue.peek() == null && mNetworkQueue.peek() == null) { Log.d(TAG, "There's no ble command in the queue."); mBleOperationInProgress = false; } else { mBleOperationInProgress = true; BleCommand bleCommand; if (mNetworkQueue.peek() != null && mMqttQueue.peek() != null) { if (rr) { bleCommand = mMqttQueue.poll(); } else { bleCommand = mNetworkQueue.poll(); } rr = !rr; } else if (mNetworkQueue.peek() != null) { bleCommand = mNetworkQueue.poll(); } else { bleCommand = mMqttQueue.poll(); } Log.d(TAG, "Processing BLE command: " + bleCommand.getType() + " remaining mqtt queue " + mMqttQueue.size() + ", network queue " + mNetworkQueue.size()); boolean commandSent = false; switch (bleCommand.getType()) { case WRITE_DESCRIPTOR: if (writeDescriptor(bleCommand.getServiceUuid(), bleCommand.getCharacteristicUuid())) { commandSent = true; } break; case WRITE_CHARACTERISTIC: if (writeCharacteristic(bleCommand.getServiceUuid(), bleCommand.getCharacteristicUuid(), bleCommand.getData())) { commandSent = true; } break; case READ_CHARACTERISTIC: if (readCharacteristic(bleCommand.getServiceUuid(), bleCommand.getCharacteristicUuid())) { commandSent = true; } break; case DISCOVER_SERVICES: if (mBluetoothGatt.discoverServices()) { commandSent = true; } else { Log.e(TAG, "Failed to discover services!"); } break; case REQUEST_MTU: if (mBluetoothGatt.requestMtu(ByteBuffer.wrap(bleCommand.getData()).getInt())) { commandSent = true; } else { Log.e(TAG, "Failed to set MTU."); } break; default: Log.w(TAG, "Unknown Ble command, cannot process."); } if (commandSent) { mHandler.postDelayed(resetOperationInProgress, BLE_COMMAND_TIMEOUT); } else { mHandler.post(resetOperationInProgress); } } } mutex.release(); } catch (InterruptedException e) { Log.e(TAG, "Mutex error", e); } } private Runnable resetOperationInProgress = new Runnable() { @Override public void run() { Log.e(TAG, "Ble command failed to be sent OR timeout after " + BLE_COMMAND_TIMEOUT + "ms"); // If current ble command timed out, process the next ble command. if (mBluetoothDevice.getBondState() != BluetoothDevice.BOND_BONDING) { processNextBleCommand(); } } }; private void processNextBleCommand() { mHandler.removeCallbacks(resetOperationInProgress); mBleOperationInProgress = false; processBleCommandQueue(); } private boolean writeDescriptor(final String serviceUuid, final String characteristicUuid) { BluetoothGattCharacteristic characteristic = getCharacteristic(serviceUuid, characteristicUuid); if (characteristic != null) { mBluetoothGatt.setCharacteristicNotification(characteristic, true); BluetoothGattDescriptor descriptor = characteristic.getDescriptor( convertFromInteger(0x2902)); if (descriptor != null) { descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); mBluetoothGatt.writeDescriptor(descriptor); return true; } else { Log.w(TAG, "There's no such descriptor on characteristic: " + characteristicUuid); } } return false; } private boolean writeCharacteristic(final String serviceUuid, final String characteristicUuid, final byte[] value) { BluetoothGattCharacteristic characteristic = getCharacteristic(serviceUuid, characteristicUuid); if (characteristic != null) { characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); Log.d(TAG, "<-<-<- Writing to characteristic: " + uuidToName.get(characteristicUuid) + " with data: " + bytesToHexString(value)); mValueWritten = value; characteristic.setValue(value); if (!mBluetoothGatt.writeCharacteristic(characteristic)) { mRWinProgress = false; Log.e(TAG, "Failed to write characteristic."); } else { mRWinProgress = true; return true; } } return false; } private BluetoothGattCharacteristic getCharacteristic(final String serviceUuid, final String characteristicUuid) { BluetoothGattService service = mBluetoothGatt.getService(UUID.fromString(serviceUuid)); if (service == null) { Log.w(TAG, "There's no such service found with uuid: " + serviceUuid); return null; } BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID.fromString(characteristicUuid)); if (characteristic == null) { Log.w(TAG, "There's no such characteristic with uuid: " + characteristicUuid); return null; } return characteristic; } private boolean readCharacteristic(final String serviceUuid, final String characteristicUuid) { BluetoothGattCharacteristic characteristic = getCharacteristic(serviceUuid, characteristicUuid); if (characteristic != null) { Log.d(TAG, "<-<-<- Reading from characteristic: " + uuidToName.get(characteristicUuid)); if (!mBluetoothGatt.readCharacteristic(characteristic)) { mRWinProgress = false; Log.e(TAG, "Failed to read characteristic."); } else { mRWinProgress = true; return true; } } return false; } private static void describeGattServices(List<BluetoothGattService> gattServices) { for (BluetoothGattService service : gattServices) { Log.d(TAG, "GattService: " + service.getUuid()); List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics(); for (BluetoothGattCharacteristic characteristic : characteristics) { Log.d(TAG, " |-characteristics: " + (uuidToName.containsKey(characteristic.getUuid().toString()) ? uuidToName.get(characteristic.getUuid().toString()) : characteristic.getUuid())); } } } private static UUID convertFromInteger(int i) { final long MSB = 0x0000000000001000L; final long LSB = 0x800000805f9b34fbL; long value = i & 0xFFFFFFFF; return new UUID(MSB | (value << 32), LSB); } private static String bytesToHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(bytes.length * 2); Formatter formatter = new Formatter(sb); for (int i = 0; i < bytes.length; i++) { formatter.format("%02x", bytes[i]); if (!VDBG && i > 10) { break; } } return sb.toString(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; AmazonFreeRTOSDevice aDevice = (AmazonFreeRTOSDevice) obj; return Objects.equals(aDevice.mBluetoothDevice.getAddress(), mBluetoothDevice.getAddress()); } }