package dk.silverbullet.telemed.device.nonin;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.util.Log;
import dk.silverbullet.telemed.device.DeviceInitialisationException;
import dk.silverbullet.telemed.device.bluetooth.BluetoothConnector;
import dk.silverbullet.telemed.device.nonin.packet.*;
import dk.silverbullet.telemed.utils.Util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
public class NoninController extends Thread implements PacketReceiver, SaturationController {
private static final String TAG = Util.getTag(NoninController.class);
private static final Pattern DEVICE_NAME_PATTERN = Pattern.compile("Nonin_Medical_Inc._\\d{6}");
private static final String MAC_ADDRESS_FOR_NONIN_MEDICAL_INC = "00:1C:05:";
private static final UUID SERIAL_SERVICE_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private static final int FIRST_TRY_TIMEOUT_IN_SECONDS = 45;
private static final int SECOND_TRY_TIMEOUT_IN_SECONDS = 55; //The device seems to take around 10 seconds to turn off
private static final int MS_MAX_DATA_WAIT_TIME = 5000; // Max time in 1/1000'th seconds to wait before giving up listening for an answer
private final BluetoothConnector connector = new BluetoothConnector();
private final BluetoothDevice device;
private final Object writeSemaphore = new Object();
private volatile boolean running;
private BluetoothSocket socket;
private InputStream inputStream;
private OutputStream outputStream;
private NoninPacketCollector packetCollector;
private SaturationPulseListener listener;
private String serialNumber;
private SaturationAndPulse lastMeasurement;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private ScheduledFuture<?> firstTimeout;
private ScheduledFuture<?> lastTimeout;
private boolean timeoutStarted;
private static long timeOfLastDisconnectedSocket=0;
private static boolean shouldWaitWhenReconnecting=false;
// Must be called when we have connected on the socket interface to the nonin
private void socketConnected() {
// Remember we were connected to the Nonin
shouldWaitWhenReconnecting = true;
}
// Should be called when we detect a disconnected socket
private void socketDisconnected() {
if(shouldWaitWhenReconnecting) {
// Remember when we were disconnected from the Nonin
timeOfLastDisconnectedSocket = System.currentTimeMillis();
}
}
// Must be called before trying to connect to the Nonin, it will
// ensure that minimum 15s has passed since the connection was lost
// if socketDisconnected hasn't been called, it will just wait
// 15s
//
// NOTE: The function will max wait 15s since "socketConnected" was called
private void ensure15sSocketDelay() throws InterruptedException {
// Bail if we don't care about waiting
if(!shouldWaitWhenReconnecting) return;
// We have a default wait time of 15s
long waitTimeMS = 15000;
// Check that we have a valid timestamp (we might have been connected,
// but socketDisconnected wasn't called
if(0 != timeOfLastDisconnectedSocket) {
// The wait time, is the waitTimeMS minus the time already passed,
// note that the result might be negative
waitTimeMS -= System.currentTimeMillis() - timeOfLastDisconnectedSocket;
}
// Wait waitTimeMS if waitTime is positive
if(0<waitTimeMS) {
sleep(waitTimeMS);
}
// We are good as new, so reset everything
timeOfLastDisconnectedSocket = 0;
shouldWaitWhenReconnecting = false;
}
public static SaturationController create(SaturationPulseListener listener) throws DeviceInitialisationException {
//Log.d(TAG, "Creating new noninController object!");
return new NoninController(listener);
}
public NoninController(SaturationPulseListener listener) throws DeviceInitialisationException {
this.listener = listener;
connector.initiate();
// Checks that the device has a pairing
device = connector.getDevice(DEVICE_NAME_PATTERN, MAC_ADDRESS_FOR_NONIN_MEDICAL_INC);
start();
}
@Override
public void close() {
running = false;
interrupt();
closeBluetoothSocket();
}
@Override
public void run() {
running = true;
packetCollector = new NoninPacketCollector();
packetCollector.setListener(this);
try {
while (running) {
sleep(1000);
Log.d(TAG, "Connecting...");
pullDataFromDevice();
}
} catch (InterruptedException e) {
Log.d(TAG, "Reader thread was interrupted!");
} finally {
Log.d(TAG, "Reader thread stopped!");
cancelTimeouts();
}
}
private void pullDataFromDevice() throws InterruptedException {
try {
setupSocket();
setupStreams();
packetCollector.reset();
sendGetSerialNumberCommand();
long timeOfLastCom = System.currentTimeMillis();
while (running) {
// Check if there are data available, otherwise wait,
// but don't wait longer than max wait time
while(0 == inputStream.available())
{
long waitTime = System.currentTimeMillis() - timeOfLastCom;
if(waitTime > MS_MAX_DATA_WAIT_TIME) {
throw new IOException("Device not responding, waited " + waitTime + "ms");
}
sleep(100);
}
int read = inputStream.read();
timeOfLastCom = System.currentTimeMillis();
if( read < 0) throw new IOException("Nothing to read");
// If the data wasn't as expected, try again
if(!packetCollector.receive(read))
{
Log.d(TAG, "Unexpected data, aborting...");
throw new IOException("Unexpected data received, resetting connection");
}
}
} catch (IOException ioe) { //Could not connect or the Nonin device has closed the connection.
Log.d(TAG, "Reader exception: " + ioe);
} finally {
// Close the BT socket
closeBluetoothSocket();
}
}
private void setupStreams() throws IOException {
inputStream = socket.getInputStream();
if(!timeoutStarted) { //First time we actually connect to the device, start the timeout.
listener.connected();
scheduleFirstTimeout();
}
synchronized (writeSemaphore) {
outputStream = socket.getOutputStream();
writeSemaphore.notify();
}
}
private void setupSocket() throws IOException, InterruptedException {
socket = device.createInsecureRfcommSocketToServiceRecord(SERIAL_SERVICE_UUID);
if (socket == null) {
throw new IOException("NullSocket!");
}
// Make sure 15s has passed since a connection was lost
ensure15sSocketDelay();
// Try to connect to the socket, this will throw an exception if it fails
socket.connect();
// We got a path to the Nonin, so remember this event
socketConnected();
}
private void sendGetSerialNumberCommand() {
Log.d(TAG, "sending get serial number command");
byte[] command = new byte[6];
command[0] = Byte.parseByte("02", 16);
command[1] = Byte.parseByte("74", 16);
command[2] = Byte.parseByte("02", 16);
command[3] = Byte.parseByte("02", 16);
command[4] = Byte.parseByte("02", 16);
command[5] = Byte.parseByte("03", 16);
try {
outputStream.write(command);
outputStream.flush();
} catch (IOException e) {
Log.e(TAG, "Error sending serial number command:" + e);
e.printStackTrace();
}
}
@Override
public void sendChangeDataFormatCommand() {
Log.d(TAG, "sending change data format command");
byte[] command = new byte[6];
command[0] = Byte.parseByte("02", 16);
command[1] = Byte.parseByte("70", 16);
command[2] = Byte.parseByte("02", 16);
command[3] = Byte.parseByte("02", 16);
command[4] = Byte.parseByte("08", 16);
command[5] = Byte.parseByte("03", 16);
try {
outputStream.write(command);
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
public void sendChangeDataFormatCommand2() {
Log.d(TAG, "sending change data format command2");
byte[] command = new byte[8];
command[0] = Byte.parseByte("02", 16);
command[1] = Byte.parseByte("70", 16);
command[2] = Byte.parseByte("04", 16);
command[3] = Byte.parseByte("02", 16);
command[4] = Byte.parseByte("08", 16);
command[5] = Byte.parseByte("00", 16);
command[6] = Byte.parseByte("7E", 16);
command[7] = Byte.parseByte("03", 16);
try {
outputStream.write(command);
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
private synchronized void closeBluetoothSocket() {
Log.i(TAG, "Closing Bluetooth socket");
// Remember that we have closed/lost the BT connection
socketDisconnected();
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
Log.i(TAG, "Could not close Bluetooth socket", e);
} finally {
socket = null;
}
}
private void scheduleFirstTimeout() {
timeoutStarted = true;
firstTimeout = scheduler.schedule(new Runnable() {
@Override
public void run() {
listener.firstTimeOut();
scheduleLastTimeout();
}
}, FIRST_TRY_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
}
private void scheduleLastTimeout() {
lastTimeout = scheduler.schedule(new Runnable() {
@Override
public void run() {
listener.finalTimeOut(serialNumber, lastMeasurement);
}
}, SECOND_TRY_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
}
private void cancelTimeouts() {
running = false;
if(lastTimeout != null) {
lastTimeout.cancel(true);
}
if(firstTimeout != null) {
firstTimeout.cancel(true);
}
}
@Override
public void setSerialNumber(NoninSerialNumberPacket packet) {
this.serialNumber = packet.serial;
}
@Override
public void addMeasurement(NoninMeasurementPacket packet) {
if(packet.highQuality) {
running = false;
listener.measurementReceived(serialNumber, new SaturationAndPulse(packet.sp02, packet.pulse));
} else if(!packet.measurementMissing) {
this.lastMeasurement = new SaturationAndPulse(packet.sp02, packet.pulse);
}
}
@Override
public void error(IOException e) {
listener.temporaryProblem();
}
}