/*
* The MIT License (MIT)
*
* Copyright (c) 2014 Little Robots
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package nl.littlerobots.bean;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import nl.littlerobots.bean.internal.ble.GattClient;
import nl.littlerobots.bean.internal.device.DeviceProfile.DeviceInfoCallback;
import nl.littlerobots.bean.internal.serial.GattSerialMessage;
import nl.littlerobots.bean.internal.serial.GattSerialTransportProfile;
import nl.littlerobots.bean.message.Acceleration;
import nl.littlerobots.bean.message.Callback;
import nl.littlerobots.bean.message.DeviceInfo;
import nl.littlerobots.bean.message.Led;
import nl.littlerobots.bean.message.Message;
import nl.littlerobots.bean.message.RadioConfig;
import nl.littlerobots.bean.message.ScratchData;
import nl.littlerobots.bean.message.SketchMetaData;
import okio.Buffer;
import static nl.littlerobots.bean.internal.Protocol.APP_MSG_RESPONSE_BIT;
import static nl.littlerobots.bean.internal.Protocol.MSG_ID_BL_GET_META;
import static nl.littlerobots.bean.internal.Protocol.MSG_ID_BT_ADV_ONOFF;
import static nl.littlerobots.bean.internal.Protocol.MSG_ID_BT_GET_CONFIG;
import static nl.littlerobots.bean.internal.Protocol.MSG_ID_BT_GET_SCRATCH;
import static nl.littlerobots.bean.internal.Protocol.MSG_ID_BT_SET_CONFIG;
import static nl.littlerobots.bean.internal.Protocol.MSG_ID_BT_SET_SCRATCH;
import static nl.littlerobots.bean.internal.Protocol.MSG_ID_CC_ACCEL_GET_RANGE;
import static nl.littlerobots.bean.internal.Protocol.MSG_ID_CC_ACCEL_READ;
import static nl.littlerobots.bean.internal.Protocol.MSG_ID_CC_ACCEL_SET_RANGE;
import static nl.littlerobots.bean.internal.Protocol.MSG_ID_CC_LED_READ_ALL;
import static nl.littlerobots.bean.internal.Protocol.MSG_ID_CC_LED_WRITE;
import static nl.littlerobots.bean.internal.Protocol.MSG_ID_CC_LED_WRITE_ALL;
import static nl.littlerobots.bean.internal.Protocol.MSG_ID_CC_TEMP_READ;
import static nl.littlerobots.bean.internal.Protocol.MSG_ID_SERIAL_DATA;
/**
* Interacts with the Punch Through Design Bean hardware.
*/
public class Bean implements Parcelable {
public static final Creator<Bean> CREATOR = new Creator<Bean>() {
@Override
public Bean createFromParcel(Parcel source) {
// ugly cast to fix bogus warning in Android Studio...
BluetoothDevice device = source.readParcelable(((Object) this).getClass().getClassLoader());
if (device == null) {
throw new IllegalStateException("Device is null");
}
return new Bean(device);
}
@Override
public Bean[] newArray(int size) {
return new Bean[size];
}
};
private static final String TAG = "Bean";
private BeanListener mInternalBeanListener = new BeanListener() {
@Override
public void onConnected() {
Log.w(TAG, "onConnected after disconnect from device " + getDevice().getAddress());
}
@Override
public void onConnectionFailed() {
Log.w(TAG, "onConnectionFailed after disconnect from device " + getDevice().getAddress());
}
@Override
public void onDisconnected() {
Log.w(TAG, "onDisconnected after disconnect from device " + getDevice().getAddress());
}
@Override
public void onSerialMessageReceived(byte[] data) {
Log.w(TAG, "onSerialMessageReceived after disconnect from device " + getDevice().getAddress());
}
@Override
public void onScratchValueChanged(int bank, byte[] value) {
}
};
private BeanListener mBeanListener = mInternalBeanListener;
private final GattClient mGattClient;
private final GattSerialTransportProfile.Listener mTransportListener;
private final BluetoothDevice mDevice;
private boolean mConnected;
private HashMap<Integer, List<Callback<?>>> mCallbacks = new HashMap<>(16);
private Handler mHandler = new Handler(Looper.getMainLooper());
/**
* Create a Bean using it's {@link android.bluetooth.BluetoothDevice}
* The bean will not be connected until {@link #connect(android.content.Context, BeanListener)} is called.
*
* @param device the device
*/
public Bean(BluetoothDevice device) {
mDevice = device;
mTransportListener = new GattSerialTransportProfile.Listener() {
@Override
public void onConnected() {
mConnected = true;
mHandler.post(new Runnable() {
@Override
public void run() {
mBeanListener.onConnected();
}
});
}
@Override
public void onConnectionFailed() {
mConnected = false;
mHandler.post(new Runnable() {
@Override
public void run() {
mBeanListener.onConnectionFailed();
}
});
}
@Override
public void onDisconnected() {
mCallbacks.clear();
mConnected = false;
mHandler.post(new Runnable() {
@Override
public void run() {
mBeanListener.onDisconnected();
}
});
}
@Override
public void onMessageReceived(final byte[] data) {
mHandler.post(new Runnable() {
@Override
public void run() {
handleMessage(data);
}
});
}
@Override
public void onScratchValueChanged(final int bank, final byte[] value) {
mHandler.post(new Runnable() {
@Override
public void run() {
mBeanListener.onScratchValueChanged(bank, value);
}
});
}
};
//mTransport = new GattSerialTransport(mTransportListener, device);
mGattClient = new GattClient();
mGattClient.getSerialProfile().setListener(mTransportListener);
}
/**
* Check if the bean is connected
*
* @return true if connected, false otherwise
*/
public boolean isConnected() {
return mConnected;
}
/**
* Attempt to connect to the Bean
*
* @param context the context used for connection
* @param listener the bean listener
*/
public void connect(Context context, BeanListener listener) {
if (mConnected) {
return;
}
mBeanListener = listener;
//mTransport.connect(context);
mGattClient.connect(context, mDevice);
}
/**
* Disconnect the bean
*/
public void disconnect() {
mGattClient.disconnect();
mGattClient.close();
mBeanListener = mInternalBeanListener;
}
/**
* Return the {@link android.bluetooth.BluetoothDevice} for this bean
*
* @return the device
*/
public BluetoothDevice getDevice() {
return mDevice;
}
/**
* Request the {@link nl.littlerobots.bean.message.RadioConfig}
*
* @param callback the callback for the result
*/
public void readRadioConfig(Callback<RadioConfig> callback) {
addCallback(MSG_ID_BT_GET_CONFIG, callback);
sendMessageWithoutPayload(MSG_ID_BT_GET_CONFIG);
}
/**
* Set the led values
*
* @param r red value
* @param g green value
* @param b blue value
*/
public void setLed(int r, int g, int b) {
Buffer buffer = new Buffer();
buffer.writeByte(r);
buffer.writeByte(g);
buffer.writeByte(b);
sendMessage(MSG_ID_CC_LED_WRITE_ALL, buffer);
}
/**
* Read the led state
*
* @param callback the callback for the result
*/
public void readLed(Callback<Led> callback) {
addCallback(MSG_ID_CC_LED_READ_ALL, callback);
sendMessageWithoutPayload(MSG_ID_CC_LED_READ_ALL);
}
/**
* Set the advertising flag (note: does not appear to work at this time)
*
* @param enable true to enable, false otherwise
*/
public void setAdvertising(boolean enable) {
Buffer buffer = new Buffer();
buffer.writeByte(enable ? 1 : 0);
sendMessage(MSG_ID_BT_ADV_ONOFF, buffer);
}
/**
* Request a temperature reading
*
* @param callback the callback for the result
*/
public void readTemperature(Callback<Integer> callback) {
addCallback(MSG_ID_CC_TEMP_READ, callback);
sendMessageWithoutPayload(MSG_ID_CC_TEMP_READ);
}
/**
* Request an acceleration sensor reading
*
* @param callback the callback for the result
*/
public void readAcceleration(Callback<Acceleration> callback) {
addCallback(MSG_ID_CC_ACCEL_READ, callback);
sendMessageWithoutPayload(MSG_ID_CC_ACCEL_READ);
}
/**
* Request the sketch metadata
*
* @param callback the callback for the result
*/
public void readSketchMetaData(Callback<SketchMetaData> callback) {
addCallback(MSG_ID_BL_GET_META, callback);
sendMessageWithoutPayload(MSG_ID_BL_GET_META);
}
/**
* Request a scratch bank data value
*
* @param number the scratch bank number, must be in the range 0-4 (inclusive)
* @param callback the callback for the result
*/
public void readScratchData(int number, Callback<ScratchData> callback) {
addCallback(MSG_ID_BT_GET_SCRATCH, callback);
Buffer buffer = new Buffer();
if (number < 0 || number > 5) {
throw new IllegalArgumentException("Scratch bank must be in the range of 0-4");
}
buffer.writeByte((number + 1) & 0xff);
sendMessage(MSG_ID_BT_GET_SCRATCH, buffer);
}
/**
* Set accelerometer range.
* @param range the range in G's, must be 2, 4, 8 or 16
*/
public void setAccelerometerRange(int range) {
Buffer buffer = new Buffer();
if (range != 2 && range != 4 && range != 8 && range != 16) {
throw new IllegalArgumentException("Sensitivity value must be 2, 4, 8 or 16");
}
buffer.writeByte(range & 0xff);
sendMessage(MSG_ID_CC_ACCEL_SET_RANGE, buffer);
}
/**
* Read the accelerometer range in G's
*
* @param callback the callback for the result
*/
public void readAccelerometerRange(Callback<Integer> callback) {
addCallback(MSG_ID_CC_ACCEL_GET_RANGE, callback);
sendMessageWithoutPayload(MSG_ID_CC_ACCEL_GET_RANGE);
}
/**
* Set a scratch bank data value
*
* @param number the scratch bank number, must be in the range 0-4 (inclusive)
* @param data the data to write
*/
public void setScratchData(int number, byte[] data) {
ScratchData sd = ScratchData.create(number, data);
sendMessage(MSG_ID_BT_SET_SCRATCH, sd);
}
/**
* Set a scratch bank data value.
*
* @param number the scratch bank number, must be in the range 0-4 (inclusive)
* @param data the string data
*/
public void setScratchData(int number, String data) {
ScratchData sd = ScratchData.create(number, data);
sendMessage(MSG_ID_BT_SET_SCRATCH, sd);
}
/**
* Set the {@link nl.littlerobots.bean.message.RadioConfig}
*
* @param config the configuration to set
*/
public void setRadioConfig(RadioConfig config) {
sendMessage(MSG_ID_BT_SET_CONFIG, config);
}
/**
* Send a serial message
*
* @param value the message payload
*/
public void sendSerialMessage(byte[] value) {
Buffer buffer = new Buffer();
buffer.write(value);
sendMessage(MSG_ID_SERIAL_DATA, buffer);
}
/**
* Send a serial message.
*
* @param value the message which will be converted to UTF-8 bytes.
*/
public void sendSerialMessage(String value) {
Buffer buffer = new Buffer();
try {
buffer.write(value.getBytes("UTF-8"));
sendMessage(MSG_ID_SERIAL_DATA, buffer);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public void readDeviceInfo(final Callback<DeviceInfo> callback) {
mGattClient.getDeviceProfile().getDeviceInfo(new DeviceInfoCallback() {
@Override
public void onDeviceInfo(DeviceInfo info) {
callback.onResult(info);
}
});
}
private void handleMessage(byte[] data) {
Buffer buffer = new Buffer();
buffer.write(data);
int type = (buffer.readShort() & 0xffff) & ~(APP_MSG_RESPONSE_BIT);
switch (type) {
case MSG_ID_SERIAL_DATA:
mBeanListener.onSerialMessageReceived(buffer.readByteArray());
break;
case MSG_ID_BT_GET_CONFIG:
returnConfig(buffer);
break;
case MSG_ID_CC_TEMP_READ:
returnTemperature(buffer);
break;
case MSG_ID_BL_GET_META:
returnMetaData(buffer);
break;
case MSG_ID_BT_GET_SCRATCH:
returnScratchData(buffer);
break;
case MSG_ID_CC_LED_READ_ALL:
returnLed(buffer);
break;
case MSG_ID_CC_ACCEL_READ:
returnAcceleration(buffer);
break;
case MSG_ID_CC_ACCEL_GET_RANGE:
returnAccelerometerRange(buffer);
break;
case MSG_ID_CC_LED_WRITE:
// ignore this response, it appears to be only an ack
break;
default:
Log.e(TAG, "Received message of unknown type " + Integer.toHexString(type));
disconnect();
break;
}
}
private void returnAccelerometerRange(Buffer buffer) {
Callback<Integer> callback = getFirstCallback(MSG_ID_CC_ACCEL_GET_RANGE);
if (callback != null) {
callback.onResult(buffer.readByte() & 0xff);
}
}
private void returnAcceleration(Buffer buffer) {
Callback<Acceleration> callback = getFirstCallback(MSG_ID_CC_ACCEL_READ);
if (callback != null) {
callback.onResult(Acceleration.fromPayload(buffer));
}
}
private void returnLed(Buffer buffer) {
Callback<Led> callback = getFirstCallback(MSG_ID_CC_LED_READ_ALL);
if (callback != null) {
callback.onResult(Led.fromPayload(buffer));
}
}
private void returnScratchData(Buffer buffer) {
Callback<ScratchData> callback = getFirstCallback(MSG_ID_BT_GET_SCRATCH);
if (callback != null) {
callback.onResult(ScratchData.fromPayload(buffer));
}
}
private void returnMetaData(Buffer buffer) {
Callback<SketchMetaData> callback = getFirstCallback(MSG_ID_BL_GET_META);
if (callback != null) {
callback.onResult(SketchMetaData.fromPayload(buffer));
}
}
private void returnTemperature(Buffer buffer) {
Callback<Integer> callback = getFirstCallback(MSG_ID_CC_TEMP_READ);
if (callback != null) {
callback.onResult((int) buffer.readByte());
}
}
private void returnConfig(Buffer data) {
RadioConfig config = RadioConfig.fromPayload(data);
Callback<RadioConfig> callback = getFirstCallback(MSG_ID_BT_GET_CONFIG);
if (callback != null) {
callback.onResult(config);
}
}
private void addCallback(int type, Callback<?> callback) {
List<Callback<?>> callbacks = mCallbacks.get(type);
if (callbacks == null) {
callbacks = new ArrayList<>(16);
mCallbacks.put(type, callbacks);
}
callbacks.add(callback);
}
@SuppressWarnings("unchecked")
private <T> Callback<T> getFirstCallback(int type) {
List<Callback<?>> callbacks = mCallbacks.get(type);
if (callbacks == null || callbacks.isEmpty()) {
Log.w(TAG, "Got response without callback!");
return null;
}
return (Callback<T>) callbacks.remove(0);
}
private void sendMessage(int type, Message message) {
Buffer buffer = new Buffer();
buffer.writeByte((type >> 8) & 0xff);
buffer.writeByte(type & 0xff);
buffer.write(message.toPayload());
GattSerialMessage serialMessage = GattSerialMessage.fromPayload(buffer.readByteArray());
mGattClient.getSerialProfile().sendMessage(serialMessage.getBuffer());
}
private void sendMessage(int type, Buffer payload) {
Buffer buffer = new Buffer();
buffer.writeByte((type >> 8) & 0xff);
buffer.writeByte(type & 0xff);
if (payload != null) {
try {
buffer.writeAll(payload);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
GattSerialMessage serialMessage = GattSerialMessage.fromPayload(buffer.readByteArray());
mGattClient.getSerialProfile().sendMessage(serialMessage.getBuffer());
}
private void sendMessageWithoutPayload(int type) {
sendMessage(type, (Buffer) null);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(mDevice, 0);
}
}