package dk.silverbullet.telemed.device.monica;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.util.Log;
import dk.silverbullet.telemed.device.*;
import dk.silverbullet.telemed.device.monica.packet.IBlockMessage;
import dk.silverbullet.telemed.device.monica.packet.MonicaMessage;
import dk.silverbullet.telemed.device.monica.packet.MonicaPacketCollector;
import dk.silverbullet.telemed.device.monica.packet.PacketReceiver;
import dk.silverbullet.telemed.device.monica.packet.states.ReceiverState;
import dk.silverbullet.telemed.utils.DataLogger;
import dk.silverbullet.telemed.utils.Util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
public class MonicaBluetoothIOController extends Thread implements PacketReceiver {
private static final String TAG = Util.getTag(MonicaBluetoothIOController.class);
private static final UUID SERIAL_SERVICE_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private static final int MESSAGE_TIMEOUT = 1000;
private static final long READWRITE_DELAY = 100; // Number of ms to wait between read until write will be allowed.
private final MonicaPacketCollector packetCollector = new MonicaPacketCollector();
private final Queue<MonicaMessage> messageQueue = new LinkedList<MonicaMessage>();
private final BluetoothDevice device;
private final BluetoothAdapter btAdapter;
private final String deviceName;
private BluetoothSocket socket;
private InputStream inputStream;
private OutputStream outputStream;
private final Object writeSemaphore = new Object();
private boolean running = true;
private long readTime;
public MonicaBluetoothIOController() throws IOException, InterruptedException, DeviceInitialisationException {
packetCollector.setListener(this);
btAdapter = BluetoothAdapter.getDefaultAdapter();
// If the adapter is null, then Bluetooth is not supported
if (btAdapter == null) {
throw new BluetoothNotAvailableException();
}
device = getDevice();
this.deviceName = device.getName();
// Increase the thread priority in order to minimize timing issues.
setPriority((NORM_PRIORITY + MAX_PRIORITY) / 2);
start();
}
@Override
public void run() {
int retries = 0;
try {
while (running) {
sleep(Math.min(100 + retries * 500, 3000));
retries++;
Log.d(TAG, "Connecting...");
try {
socket = device.createInsecureRfcommSocketToServiceRecord(SERIAL_SERVICE_UUID);
if (socket == null) {
throw new IOException("NullSocket!");
}
socket.connect();
inputStream = socket.getInputStream();
Log.d(TAG, "Read is working, now open output!");
synchronized (writeSemaphore) {
readTime = System.currentTimeMillis();
outputStream = socket.getOutputStream();
writeSemaphore.notify();
}
Log.d(TAG, "Output opened, now start working!");
int read = inputStream.read();
packetCollector.reset();
synchronized (writeSemaphore) {
readTime = System.currentTimeMillis();
}
retries = 0;
while (read >= 0) {
if (!running) {
throw new IOException("Reader thread requested to stop!");
}
packetCollector.receive((byte) read);
long processingTime = System.currentTimeMillis() - readTime;
if (processingTime > 10) {
Log.d(TAG, "Byte processing time too high: " + processingTime);
}
read = inputStream.read();
synchronized (writeSemaphore) {
readTime = System.currentTimeMillis();
}
}
throw new IOException("EOF from stream!");
} catch (IOException ioe) {
Log.d(TAG, "Reader exception: " + ioe);
if (socket != null) {
closeSocket(socket);
}
socket = null;
}
}
} catch (InterruptedException e1) {
Log.d(TAG, "Reader thread was interrupted!");
} finally {
Log.d(TAG, "Reader thread stopped!");
running = false;
}
}
@Override
public void receive(MonicaMessage packet) {
synchronized (messageQueue) {
messageQueue.add(packet);
messageQueue.notifyAll();
}
}
public String getMacAddress() {
return device.getAddress();
}
public int messagesWaiting() {
synchronized (messageQueue) {
return messageQueue.size();
}
}
public MonicaMessage readMessage(long timeout) throws IOException, MessageTimeout {
synchronized (messageQueue) {
long now = System.currentTimeMillis();
long deadline = now + timeout;
while (messageQueue.isEmpty() && now < deadline) {
try {
messageQueue.wait(deadline - now);
} catch (InterruptedException e) {
// Ignore
}
now = System.currentTimeMillis();
}
if (messageQueue.size() > 0) {
return messageQueue.remove();
} else
throw new MessageTimeout();
}
}
public void clearReadQueue() {
synchronized (messageQueue) {
messageQueue.clear();
}
}
public void close() {
running = false;
BluetoothSocket theSocket = socket;
socket = null;
if (theSocket != null) {
closeSocket(theSocket);
}
}
public void writeMessage(byte[] bytes) throws IOException {
writeMessage(bytes, 30, 30000); // Sdo
}
public void writeMessage(byte[] bytes, int retries, long timeout) throws IOException {
// Note: Should now handle embedded ETX codes!
ByteArrayOutputStream buffer2 = new ByteArrayOutputStream(25);
buffer2.write(ReceiverState.DLE);
buffer2.write(ReceiverState.STX);
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
if (b == ReceiverState.DLE)
buffer2.write(ReceiverState.DLE);
buffer2.write(b);
}
buffer2.write(ReceiverState.DLE);
buffer2.write(ReceiverState.ETX);
short crc = Util.calcCRC16(buffer2.toByteArray());
buffer2.write((crc >> 8) & 0xFF);
buffer2.write(crc & 0xFF);
Log.d(TAG, "CRC check: " + Util.calcCRC16(buffer2.toByteArray()));
long now = System.currentTimeMillis();
long deadline = now + timeout;
for (int tries = 0; tries < retries && running && now < deadline; tries++) {
try {
synchronized (writeSemaphore) {
long timeToWait = readTime + READWRITE_DELAY - System.currentTimeMillis();
while (timeToWait > 0) {
writeSemaphore.wait(timeToWait);
timeToWait = readTime + READWRITE_DELAY - System.currentTimeMillis();
}
try {
if (outputStream != null) {
Date writeTime = new Date();
outputStream.write(buffer2.toByteArray());
DataLogger.logOutput(writeTime, bytes);
return; // Success!
}
} catch (IOException ioe) {
Log.d(TAG, "Writing exception: " + ioe);
}
Log.d(TAG, "Writer waiting for outputStream...");
writeSemaphore.wait(2000); // Consider semaphore signal instead of polling
}
} catch (InterruptedException e) {
throw new IOException("Writing was interrupted!");
}
now = System.currentTimeMillis();
}
if (!running)
throw new IOException("Writing: Reader stopped!");
else
throw new MessageTimeout("Timeout writing message!");
}
public <T> T writeAndRead(byte[] message, Class<T> type) throws IOException {
return writeAndRead(message, type, 1, MESSAGE_TIMEOUT);
}
@SuppressWarnings("unchecked")
public <T> T writeAndRead(byte[] message, Class<T> type, int retries, int timeout) throws IOException {
for (int tries = 0; tries < retries; tries++) {
if (message != null)
writeMessage(message);
long now = System.currentTimeMillis();
long deadline = now + timeout;
while (now < deadline) {
MonicaMessage msg = readMessage(deadline - now);
if (type.isAssignableFrom(msg.getClass())) {
return (T) msg; // Yes, GOT IT! :)
}
now = System.currentTimeMillis();
}
}
throw new MessageTimeout(Util.getTag(type) + "-message type not received!");
}
void checkDevice() throws IOException, DeviceNotFoundException, UnknownFirmwareVersionException {
// Test the device ID and version
Log.d(TAG, "Check device...");
IBlockMessage info = writeAndRead(MessageFactory.getInfoMessage(), IBlockMessage.class, 5, 3000);
Log.d(TAG, "Info: " + info);
if (!"AN24V1A30A".equals(info.getDeviceType())) {
Log.d(TAG, "Unknown device type: " + info.getDeviceType());
throw new DeviceNotFoundException();
}
if (!"000000005900".equals(info.getDeviceVersion())) {
Log.d(TAG, "Bad/unknown device firmware version: " + info);
throw new UnknownFirmwareVersionException(info.getDeviceVersion());
}
}
private BluetoothDevice getDevice() throws IOException, InterruptedException, DeviceInitialisationException {
if (!btAdapter.isEnabled()) {
Log.d(TAG, "Sorry, Bluetooth is not enabled!");
throw new BluetoothDisabledException();
}
BluetoothDevice result = null;
for (BluetoothDevice potentialDevice : btAdapter.getBondedDevices()) {
String address = potentialDevice.getAddress();
String name = potentialDevice.getName();
Log.d(TAG, name + ": " + address);
// MONICA
if (address.startsWith("00:80:98")) {
if (result != null)
throw new AmbiguousDeviceException();
result = potentialDevice;
}
}
if (result == null) {
throw new DeviceNotFoundException();
}
return result;
}
private void closeSocket(BluetoothSocket socketToClose) {
try {
socketToClose.close();
} catch (IOException e) {
// Ignore!
}
}
public String getDeviceName() {
return this.deviceName;
}
}