/*
Copyright (C) 2013-2014, TecVis LP, support@tecvis.co.uk
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation as version 2.1 of the License.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this library; if not, write to the Free Software Foundation, Inc.,
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.airs.handlers;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Semaphore;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import com.airs.R;
import com.airs.helper.SerialPortLogger;
import com.airs.platform.HandlerManager;
import com.airs.platform.History;
import com.airs.platform.Sensor;
import com.airs.platform.SensorRepository;
/**
* Class to read Zephyr HxM related sensors, specifically the HL, HP, HI sensor
* @see Handler
*/
@SuppressLint("NewApi")
public class HeartMonitorHandler implements Handler, Runnable
{
// HMX variables
// private static final int HMX_STX = 0;
// private static final int HMX_MSG_ID = 1;
// private static final int HMX_DLC = 2;
private static final int HMX_BATTERY = 8;
private static final int HMX_PULSE = 9;
// private static final int HMX_DISTANCE = 47;
private static final int HMX_INSTANCE = 49;
// private static final int HMX_STRIDE = 51;
// UUID for getting heart rate measurements
private final static UUID UUID_HEART_RATE_MEASUREMENT = UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb");
private final static UUID UUID_BATTERY_SERVICE = UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb");
private final static UUID UUID_BATTERY_LEVEL = UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb");
private final static UUID CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
// Device type
private static final int ZEPHYR_HMX_BT = 0;
private static final int BT_SMART = 1;
// initialize old sensor values with float of zero
private int last_battery = 0;
private int last_sent_battery = 0;
private int last_pulse = 0;
private int last_instance = 0;
private int battery_LE = 0;
private int pulse_LE = 0;
private int time_window = 1;
// We will only have one instance of connection
private static final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private BluetoothGattCharacteristic battery_characteristic;
private BluetoothGatt mBluetoothGatt;
private mBluetoothGattCallback mGattCallback;
private BluetoothAdapter mBtAdapter;
private BluetoothDevice device;
private BluetoothSocket mmSocket;
private InputStream inputStream;
private String BTAddress;
// context for history
private Context airs;
// indicator for connectivity
private boolean connected = false, tried = false, use_monitor = true, shutdown = false, first_connecting = true;
private Semaphore pulse_semaphore = new Semaphore(1);
private Semaphore battery_semaphore = new Semaphore(1);
private Semaphore instance_semaphore = new Semaphore(1);
private Semaphore pulseLE_semaphore = new Semaphore(1);
private Semaphore batteryLE_semaphore = new Semaphore(1);
// type data
private int deviceType = ZEPHYR_HMX_BT;
//`ACC data being read (max)
private byte[] reading = null;
private Thread runnable;
private void debug(String msg)
{
SerialPortLogger.debug(msg);
}
private void wait(Semaphore sema)
{
try
{
sema.acquire();
}
catch(Exception e)
{
}
}
/**
* Constructor, allocating all necessary resources for the handler
* Here, it's arming the semaphore
* @param airs Reference to the calling {@link android.content.Context}
*/
public HeartMonitorHandler(Context airs)
{
// save for later
this.airs = airs;
// should handler be disabled?
if (HandlerManager.readRMS_b("HeartMonitorHandler::BTON", false) == false)
{
debug("HeartMonitorHandler::BT OFF");
use_monitor = false;
}
// what type of handler?
deviceType = Integer.parseInt(HandlerManager.readRMS("HeartMonitorHandler::BTType", "0"));
Log.e("AIRS", "chose device type "+String.valueOf(deviceType));
// arm semaphores
wait(pulse_semaphore);
wait(battery_semaphore);
wait(instance_semaphore);
wait(pulseLE_semaphore);
wait(batteryLE_semaphore);
// create callback if API level is fine
if (Build.VERSION.SDK_INT>=18)
mGattCallback = new mBluetoothGattCallback();
}
/**
* Method to release all handler resources
* Here, we release all handler semaphores as well as close the BT socket, closing also the BT read thread
* @see com.airs.handlers.Handler#destroyHandler()
*/
public void destroyHandler()
{
// signal shutdown
shutdown = true;
// shutdown read thread
if (runnable != null)
runnable.interrupt();
// release all semaphores for unlocking the Acquire() threads
pulse_semaphore.release();
pulse_semaphore.release();
instance_semaphore.release();
pulseLE_semaphore.release();
batteryLE_semaphore.release();
switch(deviceType)
{
case ZEPHYR_HMX_BT:
if (inputStream != null)
{
try
{
inputStream.close();
}
catch(Exception e)
{
}
}
if (mmSocket != null)
{
try
{
mmSocket.close();
}
catch(Exception e)
{
}
}
break;
case BT_SMART:
if (mBluetoothGatt != null)
{
Log.e("AIRS", "Shutting down BTLE device");
mBluetoothGatt.close();
mBluetoothGatt.disconnect();
}
mBluetoothGatt = null;
break;
}
connected = false;
}
/**
* Method to acquire sensor data
* Here, we first try to connect to the Zephyr for an initial connection (but only once!)
* then, it's a simple wait for the semaphores to be released
* @param sensor String of the sensor symbol
* @param query String of the query to be fulfilled - not used here
* @see com.airs.handlers.Handler#Acquire(java.lang.String, java.lang.String)
*/
synchronized public byte[] Acquire(String sensor, String query)
{
// are we shutting down?
if (shutdown == true)
return null;
// if not connected, try now!
if (connected == false)
{
// if we haven't tried before, do now (so that we only do once!)
if (tried == false)
{
// open port and create serial reading thread
if (ComPortInit() == true)
{
connected = true;
runnable = new Thread(this);
runnable.start();
}
else
{
debug("HeartMonitorHandler::ComPort initialization failed");
use_monitor = false;
// invalidate all sensors since we couldn't connect -> this will gracefully terminate any acquisition thread
SensorRepository.setSensorStatus("HL", Sensor.SENSOR_INVALID, "Could not connect to HxM", Thread.currentThread());
SensorRepository.setSensorStatus("HP", Sensor.SENSOR_INVALID, "Could not connect to HxM", Thread.currentThread());
SensorRepository.setSensorStatus("HI", Sensor.SENSOR_INVALID, "Could not connect to HxM", Thread.currentThread());
return null;
}
// we've tried!
tried = true;
}
}
// garbage collect the old stuff
reading = null;
// acquire data and send out
try
{
switch(sensor.charAt(1))
{
case 'L' :
// block until semaphore available
wait(battery_semaphore);
if (last_sent_battery != last_battery)
{
reading = new byte[6];
reading[0] = (byte)sensor.charAt(0);
reading[1] = (byte)sensor.charAt(1);
reading[2] = (byte)0;
reading[3] = (byte)0;
reading[4] = (byte)0;
reading[5] = (byte)(last_battery & 0xff);
last_sent_battery = last_battery;
}
break;
case 'P' :
// block until semaphore available
wait(pulse_semaphore);
if (last_pulse != 0)
{
reading = new byte[6];
reading[0] = (byte)sensor.charAt(0);
reading[1] = (byte)sensor.charAt(1);
reading[2] = (byte)0;
reading[3] = (byte)0;
reading[4] = (byte)0;
reading[5] = (byte)(last_pulse & 0xff);
}
break;
case 'I' :
// block until semaphore available
wait(instance_semaphore);
reading = new byte[6];
reading[0] = (byte)sensor.charAt(0);
reading[1] = (byte)sensor.charAt(1);
reading[2] = (byte)0;
reading[3] = (byte)0;
reading[4] = (byte)0;
reading[5] = (byte)(last_instance & 0xff);
break;
default:
// indicate finished reading
return null;
}
}
catch (Exception e)
{
debug("HeartMonitorHandler:Acquire: Exception: " + e.toString());
}
// return readings
return reading;
}
/**
* Method to share the last value of the given sensor
* @param sensor String of the sensor symbol to be shared
* @return human-readable string of the last sensor value
* @see com.airs.handlers.Handler#Share(java.lang.String)
*/
public String Share(String sensor)
{
switch(sensor.charAt(1))
{
case 'P' :
return "My current pulse is " + String.valueOf(last_pulse);
case 'I' :
return "My current instant speed is " + String.valueOf((double)last_instance/10.0f);
case 'L' :
return "My current battery is " + String.valueOf(last_battery);
}
return null;
}
/**
* Method to view historical chart of the given sensor symbol - supporting timeline for heart rate and instant speed
* @param sensor String of the symbol for which the history is being requested
* @see com.airs.handlers.Handler#History(java.lang.String)
*/
public void History(String sensor)
{
switch(sensor.charAt(1))
{
case 'P':
History.timelineView(airs, "Heart rate [bpm]", "HP");
break;
case 'I':
History.timelineView(airs, "Instant Speed [m/s]", "HI");
break;
}
}
/**
* Method to discover the sensor symbols support by this handler
* As the result of the discovery, appropriate {@link com.airs.platform.Sensor} entries will be added to the {@link com.airs.platform.SensorRepository}, if the monitor is selected by the user to be used
* This does not mean that the sensor has been found via BT!
* @see com.airs.handlers.Handler#Discover()
* @see com.airs.platform.Sensor
* @see com.airs.platform.SensorRepository
*/
public void Discover()
{
if (use_monitor == true)
{
// currently, only heartrate is supported for BT Smart devices
switch(deviceType)
{
case ZEPHYR_HMX_BT:
SensorRepository.insertSensor(new String("HI"), new String("m/s"), airs.getString(R.string.HI_d), airs.getString(R.string.HI_e), new String("int"), -1, 0, 200, true, 1000, this);
case BT_SMART:
SensorRepository.insertSensor(new String("HL"), new String("%"), airs.getString(R.string.HL_d), airs.getString(R.string.HL_e), new String("int"), 0, 0, 100, true, 10000, this);
SensorRepository.insertSensor(new String("HP"), new String("bpm"), airs.getString(R.string.HP_d), airs.getString(R.string.HP_e), new String("int"), 0, 0, 200, true, 1000, this);
break;
}
}
}
/**
* Initialize the serial port with parameter of COM_PORT and BAUDRATE
* @return success or not
*/
@SuppressLint("NewApi")
private boolean ComPortInit()
{
// this phone version read the BT address through the RMS entry and connects to it
try
{
// Get the local Bluetooth adapter
mBtAdapter = BluetoothAdapter.getDefaultAdapter();
// if there's no BT adapter, return without putting sensors in repository
if (mBtAdapter == null)
return false;
time_window = HandlerManager.readRMS_i("HeartMonitorHandler::Timewindow", 5);
switch(deviceType)
{
case ZEPHYR_HMX_BT:
// read BT address from RMS
BTAddress = HandlerManager.readRMS("HeartMonitorHandler::BTStore", "00:07:80:5A:3E:7E");
// now get remote device for connection
device = mBtAdapter.getRemoteDevice(BTAddress);
// this is how it should be done with proper pairing
mmSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
// now connect
mmSocket.connect();
if (mmSocket != null)
inputStream = mmSocket.getInputStream();
else
return false;
if (mmSocket != null && inputStream != null)
return true;
else
return false;
case BT_SMART:
// read BT Smart address from RMS
BTAddress = HandlerManager.readRMS("HeartMonitorHandler::BTSmartStore", "00:07:80:5A:3E:7E");
// now get remote device for connection
device = mBtAdapter.getRemoteDevice(BTAddress);
// now connect to device
mBluetoothGatt = device.connectGatt(airs, false, mGattCallback);
if (mBluetoothGatt != null)
{
Log.e("AIRS", "Connect to BTLE device : " + BTAddress);
return true;
}
else
{
Log.e("AIRS", "Cannot connect to BTLE device");
return false;
}
}
}
catch (Exception e)
{
debug("ComPort initialization failed!");
return false;
}
return false;
}
private byte readfromBT()
{
int BT = -1;
// read as long as we can't get anything useful
do
{
try
{
do
{
// read single byte
BT = inputStream.read();
}while(BT == -1);
}
catch(Exception e)
{
try
{
mmSocket.close();
inputStream.close();
}
catch(Exception e2)
{
}
mmSocket = null;
inputStream = null;
}
// if input stream does not exist anymore, try to reconnect
if (inputStream == null)
{
try
{
// wait until reconnection
Thread.sleep(15000);
// now get remote device for connection
device = mBtAdapter.getRemoteDevice(BTAddress);
// this is how it should be done with proper pairing
mmSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
// now connect
mmSocket.connect();
if (mmSocket != null)
inputStream = mmSocket.getInputStream();
else
mmSocket.close();
}
catch(Exception e)
{
mmSocket = null;
inputStream = null;
}
}
}while (BT==-1 && shutdown == false);
return (byte)BT;
}
/**
*
* This thread keeps on reading the BT serial port
*/
@SuppressLint("NewApi")
public void run()
{
int i;
byte header;
byte endOfMessage;
byte[] payload = null;
int payload_length = 0;
int averaging = 0;
int battery=0, pulse=0, instance=0;
int current_instance;
if (inputStream==null && deviceType == ZEPHYR_HMX_BT)
{
debug("No input stream: halt pump");
return;
}
try
{
while (shutdown == false)
{
switch(deviceType)
{
case ZEPHYR_HMX_BT:
// try to read from serial port
try
{
// read the header information
do
{
header = readfromBT();
// check for first byte (0x02) and message ID (0x26)
if (header == (byte)0x02)
{
header = readfromBT();
if (header == (byte)0x26)
{
header = readfromBT();
// extract payload length
payload_length = header;
if (payload_length != 0)
break;
}
}
}while(shutdown == false);
if (shutdown == true)
return;
// now generate payload packet
payload = new byte[payload_length];
// read rest of header
for (i=0;i<payload_length;i++)
payload[i] = readfromBT();
// read checksum
readfromBT();
// read end of message
endOfMessage = readfromBT();
// proper end of message?
if (endOfMessage == (byte)0x03)
{
int current_pulse = (int)(payload[HMX_PULSE] & 0xff);
// filter too low and too high values
if (current_pulse > 30 & current_pulse<230)
{
// now read values
battery += (int)(payload[HMX_BATTERY] & 0xff);
pulse += current_pulse;
current_instance = (int)(payload[HMX_INSTANCE] & 0xff) | (int)(payload[HMX_INSTANCE+1] & 0xff)<<8;
instance += (int)Math.floor((double)current_instance/25.6f);
// now increase averaging window
averaging++;
}
}
}
catch (Exception e)
{
debug("HeartMonitorHandler::Failed to read serial data: " + e.toString());
// invalidate all sensors since reading failed -> this will gracefully terminate any acquisition thread
SensorRepository.setSensorStatus("HL", Sensor.SENSOR_INVALID, "HxM disconnected", null);
SensorRepository.setSensorStatus("HP", Sensor.SENSOR_INVALID, "HxM disconnected", null);
SensorRepository.setSensorStatus("HI", Sensor.SENSOR_INVALID, "HxM disconnected", null);
// release semaphores for picking up the values
pulse_semaphore.release();
battery_semaphore.release();
instance_semaphore.release();
return;
}
break;
case BT_SMART:
// waiting for pulse value to arrive
if (connected == true)
wait(pulseLE_semaphore);
if (connected == true)
{
if (pulse_LE != 0)
{
pulse += pulse_LE;
// now increase averaging window
averaging++;
}
}
else
{
// trying to reconnect to heart rate monitor
while(connected == false && shutdown == false)
{
try
{
// now connect to device
// mBluetoothGatt = device.connectGatt(airs, false, mGattCallback);
mBluetoothGatt.connect();
Log.e("AIRS", "Trying to reconnect to BTLE device");
// wait for connection to succeed
Thread.sleep(15000);
}
catch(Exception e)
{
}
}
if (shutdown == false)
Log.e("AIRS", "Reconnected to BTLE device");
}
break;
}
if (shutdown == true)
return;
// reached averaging window length?
if (averaging == time_window)
{
// determine average values
last_pulse = pulse/averaging;
// depending on device type, either average the battery or read the value
switch(deviceType)
{
case ZEPHYR_HMX_BT:
last_battery = battery/averaging;
last_instance = instance/averaging;
// signal to Acquire() thread
instance_semaphore.release();
battery_semaphore.release();
break;
case BT_SMART:
// is there any battery values supported by the device?
if (battery_characteristic != null)
{
// trying to read battery level
if (mBluetoothGatt.readCharacteristic(battery_characteristic) == true)
{
// wait for battery level to return
wait(batteryLE_semaphore);
if (connected == true)
{
last_battery = battery_LE;
// now signal to Acquire() thread
battery_semaphore.release();
}
}
else
Log.e("AIRS", "Error reading battery characteristic");
}
break;
}
// reset counters
averaging = 0;
battery = pulse = instance = 0;
// release semaphores for picking up the values
if (connected == true)
pulse_semaphore.release();
}
}
}
catch(Exception e)
{
}
}
// Various callback methods defined by the BLE API.
@SuppressLint("NewApi")
private class mBluetoothGattCallback extends BluetoothGattCallback
{
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
{
if (newState == BluetoothProfile.STATE_CONNECTED)
{
Log.e("AIRS", "Successfully connected to BTLE device");
SensorRepository.setSensorStatus("HP", Sensor.SENSOR_VALID, "Connected", null);
SensorRepository.setSensorStatus("HL", Sensor.SENSOR_VALID, "Connected", null);
connected = true;
// now start discovering services
mBluetoothGatt.discoverServices();
}
else
if (newState == BluetoothProfile.STATE_DISCONNECTED)
{
SensorRepository.setSensorStatus("HP", Sensor.SENSOR_INVALID, "Disconnected", null);
SensorRepository.setSensorStatus("HL", Sensor.SENSOR_INVALID, "Disconnected", null);
connected = false;
// is it the first time we are trying to connect? If so, there's nothing to connect here
if (first_connecting == true)
{
Log.e("AIRS", "no BTLE device found, shutting down");
shutdown = true;
}
Log.e("AIRS", "Disconnected from BTLE device");
pulseLE_semaphore.release(); // signal to read thread for trying reconnection
if (batteryLE_semaphore.availablePermits() == 0)
batteryLE_semaphore.release(); // signal to read thread for trying reconnection
}
}
@Override
// New services discovered
public void onServicesDiscovered(BluetoothGatt gatt, int status)
{
int i, j;
List<BluetoothGattService> services;
BluetoothGattService service;
List<BluetoothGattCharacteristic> characteristics;
BluetoothGattCharacteristic characteristic;
if (status == BluetoothGatt.GATT_SUCCESS)
{
Log.e("AIRS", "Finished service discovery");
// get services now
services = mBluetoothGatt.getServices();
// walk through all services
for (i=0; i<services.size(); i++)
{
// get current service
service = services.get(i);
// get the service characteristics
characteristics = service.getCharacteristics();
// go through all characteristics
for (j=0; j<characteristics.size(); j++)
{
// get current characteristic
characteristic = characteristics.get(j);
// heart rate measurement
if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid()))
{
// enable notification for heart rate measurements
mBluetoothGatt.setCharacteristicNotification(characteristic, true);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
Log.e("AIRS", "Set notification for heart rate changes");
}
// battery level
if (UUID_BATTERY_LEVEL.equals(characteristic.getUuid()))
{
BluetoothGattService batteryService = mBluetoothGatt.getService(UUID_BATTERY_SERVICE);
if(batteryService != null)
{
battery_characteristic = batteryService.getCharacteristic(UUID_BATTERY_LEVEL);
if(battery_characteristic != null)
Log.e("AIRS", "Found characteristic for battery_level");
}
}
}
}
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)
{
if (status == BluetoothGatt.GATT_SUCCESS)
{
if (UUID_BATTERY_LEVEL.equals(characteristic.getUuid()))
{
// now get battery value
battery_LE = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
Log.e("AIRS", "Received battery from BTLE device: " + String.valueOf(battery_LE));
// signal to reading thread!
batteryLE_semaphore.release();
}
}
}
@Override
// Result of a characteristic changed operation
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic)
{
if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid()))
{
int flag = characteristic.getProperties();
int format = -1;
if ((flag & 0x01) != 0)
format = BluetoothGattCharacteristic.FORMAT_UINT16;
else
format = BluetoothGattCharacteristic.FORMAT_UINT8;
// now get battery value
pulse_LE = characteristic.getIntValue(format, 1);
Log.e("AIRS", "Received heartrate from BTLE device: " + String.valueOf(pulse_LE));
// signal to reading thread!
pulseLE_semaphore.release();
}
}
};
}