package com.elmz.drift.openbci;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.util.Log;
import com.ftdi.j2xx.D2xxManager;
import com.ftdi.j2xx.FT_Device;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
/**
* Created by El1t on 1/17/15.
*/
public class OpenBCIService extends Service implements BrainStateCallback
{
public static final String TAG = "OpenBCI Service";
private static final int MAX_READ_LENGTH = 512;
private static final int PACKET_LENGTH = 33;
private final float mVref = 4.5f;
private final double mAccelScale = 0.002d / Math.pow(2,4);
private double mGain = 24d;
private double mVoltScale = mVref / (Math.pow(2,23)-1) / mGain * 1000000;
private static D2xxManager sManager = null;
private ReadThread mReadThread;
private FT_Device mDevice = null;
private boolean streaming;
private byte[] overflowBuffer = new byte[MAX_READ_LENGTH*2];
private int overflowLength = 0;
int[] alphaCheckChannels = {7-1,8-1};
private Messenger mMessenger;
StreamReader mStreamReader = new StreamReader(this);
AlphaDetector mAlphaDetector = new AlphaDetector(this);
private final IBinder mBinder = new LocalBinder();
public class LocalBinder extends Binder {
public OpenBCIService getService() {
return OpenBCIService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public static final int MSG_TEST = 1;
private final Messenger mIncomingMessenger = new Messenger(new IncomingHandler());
private class IncomingHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.arg1) {
case 1:
Log.d(TAG, "Stream Start");
startDataStream();
break;
case 2:
Log.d(TAG, "Stream Stop");
stopDataStream();
break;
}
}
}
public Messenger getIncomingMessenger() {
return mIncomingMessenger;
}
final Handler INIT_HANDLER = new Handler() {
@Override
public void handleMessage(Message msg) {
final byte[] input = (byte[]) msg.obj;
final char[] decoded = Arrays.copyOf(Charset.forName("US-ASCII").decode(ByteBuffer.wrap(input)).array(), msg.arg1);
if (decoded.length > 4 && decoded[decoded.length - 3] == '$' && decoded[decoded.length - 2] == '$' && decoded[decoded.length-1] == '$') {
Log.d(TAG, "EOT received");
// Notify the LoginActivity that OpenBCI is initialized
Message notif = Message.obtain();
notif.arg1=1;
try {
mMessenger.send(notif);
} catch (Exception e1) {
Log.w(getClass().getName(), "Exception sending message", e1);
}
// new Handler().postDelayed(new Runnable() {
// @Override
// public void run() {
// writeToDevice(OpenBCICommands.START_STREAM);
// streaming = true;
// }
// }, 70);
// new Handler().postDelayed(new Runnable() {
// @Override
// public void run() {
// writeToDevice(OpenBCICommands.STOP_STREAM);
// streaming = false;
// }
// }, 20000);
} else {
Log.d(TAG, new String(decoded));
}
}
};
final Handler PACKET_HANDLER = new Handler() {
@Override
public void handleMessage(Message msg) {
final byte[] input = (byte[]) msg.obj;
System.arraycopy(input, 0, overflowBuffer, overflowLength, input.length);
if ((overflowLength + input.length)/PACKET_LENGTH > 0) {
DataPacket temp;
int index;
for (index = 0; index + PACKET_LENGTH < overflowLength + input.length; index += PACKET_LENGTH) {
// Verify integrity
if (overflowBuffer[index] != (byte) 0xA0 || overflowBuffer[index + PACKET_LENGTH - 1] != (byte) 0xC0) {
Log.d(TAG, "Invalid header/footer in packet");
continue;
}
temp = new DataPacket(8, 3);
temp.sampleIndex = (int) overflowBuffer[index + 1];
for (int j = index + 2; j < index + 26; j += 3) {
temp.values[(j - index - 2) / 3] = interpret24bitAsInt32(overflowBuffer[j],
overflowBuffer[j + 1], overflowBuffer[j + 2]) * mVoltScale;
}
for (int j = index + 26; j < index + PACKET_LENGTH - 1; j += 2) {
temp.auxValues[(j - index - 26) / 2] = interpret16bitAsInt32(overflowBuffer[j], overflowBuffer[j + 1]) * mAccelScale;
}
//temp.printToConsole();
analyze(temp);
}
System.arraycopy(overflowBuffer, index, overflowBuffer, 0, overflowLength + input.length - index);
overflowLength += input.length - index;
} else {
overflowLength += input.length;
}
}
};
void analyze(DataPacket dp) {
mStreamReader.addFrame(dp.values[0]);
mAlphaDetector.addFrames(new float[]{(float)dp.values[alphaCheckChannels[0]],(float)dp.values[alphaCheckChannels[1]]});
}
private class ReadThread extends Thread {
public ReadThread() {
this.setPriority(Thread.NORM_PRIORITY);
}
@Override
public void run() {
byte[] readData;
int dataLength;
Message msg;
while(!isInterrupted()) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
break;
}
synchronized(mDevice) {
dataLength = mDevice.getQueueStatus();
if (dataLength > 0) {
if (dataLength > MAX_READ_LENGTH) {
dataLength = MAX_READ_LENGTH;
}
readData = new byte[dataLength];
mDevice.read(readData, dataLength);
// Object: byte array
// Arg1: length of array
if (streaming) {
msg = PACKET_HANDLER.obtainMessage();
} else {
msg = INIT_HANDLER.obtainMessage();
}
msg.obj = readData;
msg.arg1 = dataLength;
if (streaming) {
PACKET_HANDLER.sendMessage(msg);
} else {
INIT_HANDLER.sendMessage(msg);
}
}
}
}
Log.d(TAG, "Reading thread stopped");
}
}
public void startDataStream() {
writeToDevice(OpenBCICommands.START_STREAM);
streaming = true;
}
public void stopDataStream() {
writeToDevice(OpenBCICommands.STOP_STREAM);
streaming = false;
}
@Override
public void blinkStart() {
// Notify the StatusFragment obtained blink start
Message notif = Message.obtain();
notif.arg1=2;
notif.arg2=1; //1 is blink start, 2 is blink end, 3 is alpha data
notif.obj=null;
try {
mMessenger.send(notif);
} catch (Exception e1) {
Log.w(getClass().getName(), "Exception sending message", e1);
}
}
@Override
public void blinkEnd(double blinkDuration) {
// Notify the StatusFragment obtained blink end
Message notif = Message.obtain();
notif.arg1=2;
notif.arg2=2; //1 is blink start, 2 is blink end, 3 is alpha data
notif.obj=blinkDuration;
try {
mMessenger.send(notif);
} catch (android.os.RemoteException e1) {
Log.w(getClass().getName(), "Exception sending message", e1);
}
}
@Override
public void alpha(AlphaDetector.DetectionData_FreqDomain[] results) {
double alphampsum = 0;
for (AlphaDetector.DetectionData_FreqDomain ddfd : results) alphampsum += ddfd.inband_vs_guard_dB;
Log.d(TAG + "Alpha", Double.toString(alphampsum/results.length));
// Notify the StatusFragment obtained alpha data
Message notif = Message.obtain();
notif.arg1=2;
notif.arg2=3; //1 is blink start, 2 is blink end, 3 is alpha data
notif.obj=results;
try {
mMessenger.send(notif);
} catch (Exception e1) {
Log.w(getClass().getName(), "Exception sending message", e1);
}
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "Creating service");
try {
sManager = D2xxManager.getInstance(this);
} catch (D2xxManager.D2xxException e) {
Log.e(TAG, "D2xx manager", e);
}
if(!sManager.setVIDPID(0x0403, 0xada1)) {
Log.d(TAG, "setVIDPID Error");
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
Log.d(TAG, "Starting OpenBCI service");
// Note: intent is null when service restarts itself
if (intent != null && intent.getExtras() != null) {
mMessenger = (Messenger) intent.getExtras().get(TAG);
}
connectDevice();
// If we get killed, after returning from here, restart
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
writeToDevice(OpenBCICommands.STOP_STREAM);
disconnectDevice();
Log.d(TAG, "Stopping OpenBCI service");
}
void configDevice() {
if (!mDevice.isOpen()) {
Log.e(TAG, "setConfig: device not open");
return;
}
mDevice.setBitMode((byte) 0, D2xxManager.FT_BITMODE_RESET);
mDevice.setBaudRate(115200);
mDevice.setDataCharacteristics(D2xxManager.FT_DATA_BITS_8, D2xxManager.FT_STOP_BITS_1, D2xxManager.FT_PARITY_NONE);
mDevice.setFlowControl(D2xxManager.FT_FLOW_NONE, (byte) 0x0b, (byte) 0x0d);
Log.d(TAG, "Config finished");
mDevice.purge(D2xxManager.FT_PURGE_TX);
mDevice.purge(D2xxManager.FT_PURGE_RX);
mDevice.restartInTask();
Log.d(TAG, "Read enabled");
streaming = false;
overflowLength = 0;
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
writeToDevice(OpenBCICommands.SOFT_RESET);
}
}, 3000);
}
void connectDevice() {
if (mDevice == null) {
if (sManager.createDeviceInfoList(this) > 0) {
mDevice = sManager.openByIndex(this, 0);
} else {
// try again in a few seconds
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
connectDevice();
}
}, 3000);
}
} else if (mDevice.isOpen()) {
Log.d(TAG, "Device already open");
return;
} else {
synchronized(mDevice) {
mDevice = sManager.openByIndex(this, 0);
}
}
if (mDevice == null) {
Log.d(TAG, "OpenBCI not found");
} else if (mDevice.isOpen()) {
configDevice();
if (mReadThread == null || !mReadThread.isInterrupted()) {
mReadThread = new ReadThread();
mReadThread.start();
}
} else {
Log.e(TAG, "Cannot connect to OpenBCI: port not open");
}
}
void disconnectDevice() {
if (mReadThread != null) {
mReadThread.interrupt();
}
try {
Thread.sleep(50);
}
catch (InterruptedException e) {
Log.d(TAG, "Thread interrupted");
}
if(mDevice != null) {
synchronized(mDevice) {
if(mDevice.isOpen()) {
writeToDevice(OpenBCICommands.STOP_STREAM);
mDevice.close();
}
}
}
// Notify the LoginActivity that OpenBCI has been disconnected
Message notif = Message.obtain();
notif.arg1 = 0;
try {
mMessenger.send(notif);
} catch (Exception e1) {
Log.w(getClass().getName(), "Exception sending message", e1);
}
}
void writeToDevice(char value) {
if (mDevice != null && mDevice.isOpen()) {
mDevice.setLatencyTimer((byte) 16);
mDevice.write(new byte[]{(byte) value}, 1);
Log.d(TAG, "Sent '" + value + "' to device.");
} else {
Log.e(TAG, "Write: mDevice not open");
}
}
private int interpret24bitAsInt32(byte a, byte b, byte c) {
//little endian
final int newInt = ((0xFF & a) << 16) | ((0xFF & b) << 8) | 0xFF & c;
if ((newInt & 0x00800000) > 0) {
return newInt | 0xFF000000;
}
return newInt & 0x00FFFFFF;
}
private int interpret16bitAsInt32(byte a, byte b) {
final int newInt = ((0xFF & a) << 8) | (0xFF & b);
if ((newInt & 0x00008000) > 0) {
return newInt | 0xFFFF0000;
}
return newInt & 0x0000FFFF;
}
}