package interdroid.swan.sensors.impl;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.ContentValues;
import android.os.Bundle;
import android.util.Log;
import interdroid.swan.R;
import interdroid.swan.sensors.AbstractConfigurationActivity;
import interdroid.swan.sensors.AbstractSwanSensor;
/**
* This sensor knows how to interface to a Bluetooth Polar Heart Rate Monitor.
*
* @author nick <palmer@cs.vu.nl>
*
*/
public class PolarHeartRate extends AbstractSwanSensor {
private static final String TAG = "PolarHeartRate Sensor";
/**
* Access to logger.
*/
private static final Logger LOG =
LoggerFactory.getLogger(PolarHeartRate.class);
/**
* This configuration activity for this sensor.
* @author nick <palmer@cs.vu.nl>
*
*/
public static class ConfigurationActivity
extends AbstractConfigurationActivity {
@Override
public final int getPreferencesXML() {
return R.xml.polar_hrm_preferences;
}
}
/** Beats per minute. */
public static final String BPM = "bpm";
/** Status. I think this is battery. */
public static final String STATUS = "status";
/** This is some sort of index: 1-15. Is this time related? */
public static final String INDEX = "index";
/** RRI values in ms. 60000 / rri = bpm. */
public static final String RRI = "rri";
/** Status text, shows the status of the connection to the sensor */
public static final String STATUS_TEXT_FIELD = "status_text";
/** The device name configuration. */
public static final String DEVICE_NAME = "deviceName";
/** The default device name to look for. */
private static final String DEFAULT_DEVICE_NAME = "Polar iWL";
/** The bluetooth adapter we use to get access to bluetooth. */
private BluetoothAdapter mBluetoothAdapter;
/** The threads which are monitoring various devices. */
private Map<String, Thread> mServiceThreads = new HashMap<String, Thread>();
/** A thread monitoring one device. */
private class DeviceThread extends Thread {
/** Time to sleep before pooling for more input to read. */
private static final int POLL_DELAY = 100;
/** Maximum byte value */
private static final int MAX_BYTE = 256;
/** Size of the protocol header. */
private static final int HEADER_SIZE = 7;
/** The maximum check value. */
private static final int CHECK_MAX = 255;
/** The start of a frame. */
private static final int FRAME_HEADER_MAGIC = 254;
/** The input stream from the device. */
private InputStream mStream;
/**
* Construct a DeviceThread to monitor the stream.
*
* @param stream the stream to monitor
*/
public DeviceThread(final InputStream stream) {
mStream = stream;
}
@Override
public void run() {
try {
LOG.debug("Built input stream.");
boolean interrupted = false;
// Avoid pressure on the GC
ContentValues values = new ContentValues();
List<String> rris = new ArrayList<String>();
while (!interrupted && !interrupted()) {
if (mStream.available() > 0) {
int header = -1;
// Ensure we have the header byte
while (header != FRAME_HEADER_MAGIC) {
header = mStream.read();
}
int size = mStream.read();
int check = mStream.read();
if (check != (CHECK_MAX - size)) {
LOG.debug("Bad packet. Skipping.");
continue;
}
// Index runs from 1 to 15 and repeats
int index = mStream.read();
// Always seeing 241 for status? Battery?
int status = mStream.read();
// This seems to be right.
int bpm = mStream.read();
// Clear the array
rris.clear();
for (int i = HEADER_SIZE; i < size; i += 2) {
rris.add(String.valueOf(mStream.read() * MAX_BYTE
+ mStream.read()));
}
storeValues(values, index, status, bpm, rris);
} else {
try {
sleep(POLL_DELAY);
} catch (InterruptedException e) {
interrupted = true;
LOG.debug(
"Interrupted while waiting for reading: {}",
interrupted());
}
}
}
} catch (IOException e) {
LOG.error("Error creating socket.", e);
} finally {
LOG.debug("Closing bluetooth socket.");
if (mStream != null) {
try {
mStream.close();
} catch (Exception e) {
LOG.error("Error closing input stream", e);
}
}
}
}
};
/**
* Store values to the DB
* @param values for reuse
* @param index the index
* @param status the status
* @param bpm the bpm
* @param rris list of rri values
*/
private void storeValues(final ContentValues values, final int index,
final int status, final int bpm, final List<String> rris) {
long now = System.currentTimeMillis();
for (String rri : rris) {
// Clear the values
values.clear();
// Store the data.
values.put(BPM, bpm);
values.put(INDEX, index);
values.put(STATUS, status);
values.put(RRI, rri);
values.put(STATUS_TEXT_FIELD, statusAsText(status));
Log.d(TAG, "New values: " + bpm + " " + index + " " + status);
putValueTrimSize(BPM, null, now, bpm);
putValueTrimSize(INDEX, null, now, index);
putValueTrimSize(STATUS, null, now, status);
putValueTrimSize(RRI, null, now, rri);
putValueTrimSize(STATUS_TEXT_FIELD, null, now, statusAsText(status));
}
}
private String statusAsText(int status){
switch(status){
case 241:
return "Connected - Active reading HR";
case 251:
return "Connected - Problem reading HR";
default:
return "Unknown: " + status;
}
}
@Override
public final void register(final String id, final String valuePath,
final Bundle configuration) throws IOException {
if (!mBluetoothAdapter.isEnabled()) {
throw new IllegalStateException("Bluetooth is not enabled.");
}
String deviceName = configuration.getString(DEVICE_NAME);
if (null == deviceName) {
deviceName = mDefaultConfiguration.getString(DEVICE_NAME);
}
boolean found = false;
if (mBluetoothAdapter.isEnabled()) {
for (BluetoothDevice device
: mBluetoothAdapter.getBondedDevices()) {
if (device.getName().equals(deviceName)) {
LOG.debug("Found paired Polar iWL {}" + device);
UUID uuid = UUID.fromString(
"00001101-0000-1000-8000-00805F9B34FB");
BluetoothSocket socket;
try {
socket = device.createRfcommSocketToServiceRecord(uuid);
socket.connect();
InputStream inStream = socket.getInputStream();
DeviceThread serviceThread = new DeviceThread(inStream);
serviceThread.start();
mServiceThreads.put(id, serviceThread);
found = true;
break;
} catch (IOException e) {
LOG.error("Unable to connect to device.", e);
throw e;
}
}
}
}
if (!found) {
throw new IllegalArgumentException(
"Unable to find bonded device to pair with name: "
+ deviceName);
}
}
@Override
public final void unregister(final String id) {
Thread thread = mServiceThreads.get(id);
if (thread != null) {
thread.interrupt();
}
}
@Override
public final void onDestroySensor() {
for (Thread thread : mServiceThreads.values()) {
thread.interrupt();
}
super.onDestroySensor();
}
@Override
public final void onConnected() {
SENSOR_NAME = "PolarHeartRate Sensor";
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
}
@Override
public final void initDefaultConfiguration(final Bundle defaults) {
defaults.putString(DEVICE_NAME, DEFAULT_DEVICE_NAME);
}
@Override
public final String[] getValuePaths() {
return new String[] { BPM };
}
}