Implemented heart rate via Bluetooth LE.

This commit is contained in:
Dennis Guse
2019-07-07 19:29:35 +02:00
parent 199f74ffad
commit 19f7636259
4 changed files with 122 additions and 328 deletions
@@ -16,287 +16,134 @@
package com.google.android.apps.mytracks.services.sensors;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorState;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import androidx.annotation.NonNull;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorState;
import java.util.UUID;
/**
* Manages bluetooth connection. It has a thread for connecting with a bluetooth
* device and a thread for performing data transmission when connected.
*
* @author Sandor Dornbush
* Manages connection to Bluetooth LE heart rate monitor.
* TODO: shutdown connection.
*/
public class BluetoothConnectionManager {
public static final UUID MY_TRACKS_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
private static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
private static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
// Message types sent to handler
public static final int MESSAGE_DEVICE_NAME = 1;
public static final int MESSAGE_READ = 2;
// Message types sent to handler
public static final int MESSAGE_DEVICE_NAME = 1;
public static final int MESSAGE_READ = 2;
public static final String KEY_DEVICE_NAME = "device_name";
// Key for storing the device name
public static final String KEY_DEVICE_NAME = "device_name";
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private final Context context;
private final Handler handler;
private SensorState sensorState;
private final BluetoothAdapter bluetoothAdapter;
private final Handler handler;
private final MessageParser messageParser;
private SensorState sensorState;
private BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices();
setState(SensorState.CONNECTED);
private ConnectThread connectThread;
private ConnectedThread connectedThread;
/**
* Constructor.
*
* @param bluetoothAdapter the bluetooth adapter
* @param handler a hander for sending messages back to the UI activity
* @param messageParser a message parser
*/
public BluetoothConnectionManager(
BluetoothAdapter bluetoothAdapter, Handler handler, MessageParser messageParser) {
this.bluetoothAdapter = bluetoothAdapter;
this.handler = handler;
this.messageParser = messageParser;
this.sensorState = SensorState.NONE;
}
/**
* Gets the sensor state.
*/
public synchronized SensorState getSensorState() {
return sensorState;
}
/**
* Sets the sensor state.
*
* @param sensorState the sensor state
*/
private synchronized void setState(Sensor.SensorState sensorState) {
this.sensorState = sensorState;
}
/**
* Resets the bluetooth connection manager.
*/
public synchronized void reset() {
cancelThreads();
setState(Sensor.SensorState.NONE);
}
/**
* Cancels all the threads.
*/
private void cancelThreads() {
if (connectThread != null) {
connectThread.cancel();
connectThread = null;
}
if (connectedThread != null) {
connectedThread.cancel();
connectedThread = null;
}
}
/**
* Connects to a bluetooth device.
*
* @param bluetoothDevice the bluetooth device
*/
public synchronized void connect(BluetoothDevice bluetoothDevice) {
Log.d(TAG, "connect to: " + bluetoothDevice);
cancelThreads();
connectThread = new ConnectThread(bluetoothDevice);
connectThread.start();
setState(Sensor.SensorState.CONNECTING);
}
/**
* Starts the ConnectedThread to read data.
*
* @param bluetoothSocket the bluetooth socket
* @param bluetoothDevice the bluetooth device
*/
private synchronized void connected(
BluetoothSocket bluetoothSocket, BluetoothDevice bluetoothDevice) {
cancelThreads();
connectedThread = new ConnectedThread(bluetoothSocket);
connectedThread.start();
// Send the device name to the handler
Message message = handler.obtainMessage(MESSAGE_DEVICE_NAME);
Bundle bundle = new Bundle();
bundle.putString(KEY_DEVICE_NAME, bluetoothDevice.getName());
message.setData(bundle);
handler.sendMessage(message);
setState(Sensor.SensorState.CONNECTED);
}
/**
* A thread to connect to a bluetooth device.
*/
private class ConnectThread extends Thread {
private final BluetoothSocket bluetoothSocket;
private final BluetoothDevice bluetoothDevice;
public ConnectThread(BluetoothDevice device) {
setName("ConnectThread-" + device.getName());
this.bluetoothDevice = device;
BluetoothSocket tmp = null;
try {
tmp = bluetoothDevice.createInsecureRfcommSocketToServiceRecord(BluetoothConnectionManager.MY_TRACKS_UUID);
} catch (IOException e) {
Log.d(TAG, "Unable to create insecure connection", e);
}
if (tmp == null) {
try {
tmp = bluetoothDevice.createRfcommSocketToServiceRecord(BluetoothConnectionManager.MY_TRACKS_UUID);
} catch (IOException e) {
Log.e(TAG, "Unable to get blueooth socket.", e);
}
}
bluetoothSocket = tmp;
}
@Override
public void run() {
if (bluetoothAdapter == null) {
BluetoothConnectionManager.this.reset();
return;
}
// Cancel discovery to prevent slow down
bluetoothAdapter.cancelDiscovery();
try {
bluetoothSocket.connect();
} catch (IOException connectException) {
Log.i(TAG, "Unable to connect.", connectException);
setState(Sensor.SensorState.DISCONNECTED);
try {
bluetoothSocket.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close blueooth socket.", e);
}
// Reset the bluetooth connection manager
BluetoothConnectionManager.this.reset();
return;
}
// Reset the ConnectThread since we are done
synchronized (BluetoothConnectionManager.this) {
connectThread = null;
}
// Start the connected thread
connected(bluetoothSocket, bluetoothDevice);
}
/**
* Cancels this thread.
*/
public void cancel() {
try {
bluetoothSocket.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close bluetooth socket.", e);
}
}
}
/**
* This thread handles data transmission when connected.
*/
private class ConnectedThread extends Thread {
private final BluetoothSocket bluetoothSSocket;
private final InputStream inputStream;
public ConnectedThread(BluetoothSocket bluetoothSocket) {
this.bluetoothSSocket = bluetoothSocket;
InputStream tmp = null;
try {
tmp = bluetoothSocket.getInputStream();
} catch (IOException e) {
Log.e(TAG, "Unable to get input stream.", e);
}
inputStream = tmp;
}
@Override
public void run() {
byte[] buffer = new byte[messageParser.getFrameSize()];
int bytes; // bytes read
int offset = 0;
// Keep listening to the inputStream while connected
while (true) {
try {
// Read from the inputStream
bytes = inputStream.read(buffer, offset, messageParser.getFrameSize() - offset);
if (bytes == -1) { throw new IOException("EOF reached."); }
offset += bytes;
if (offset != messageParser.getFrameSize()) {
// Partial frame received. Call read again to receive the rest.
continue;
}
if (!messageParser.isValid(buffer)) {
int index = messageParser.findNextAlignment(buffer);
if (index == -1) {
Log.w(TAG, "Could not find any valid data. Drop data.");
offset = 0;
continue;
//Inform about status change
Message message = handler.obtainMessage(MESSAGE_DEVICE_NAME);
Bundle bundle = new Bundle();
bundle.putString(KEY_DEVICE_NAME, gatt.getDevice().getName());
message.setData(bundle);
handler.sendMessage(message);
return;
}
Log.w(TAG, "Misaligned data. Found new message at " + index + ". Recovering...");
offset = messageParser.getFrameSize() - index;
System.arraycopy(buffer, index, buffer, 0, offset);
continue;
}
offset = 0;
// Send a copy of the obtained bytes to the handler to avoid memory
// inconsistency issues
handler.obtainMessage(MESSAGE_READ, bytes, -1, buffer.clone()).sendToTarget();
} catch (IOException e) {
Log.i(TAG, "Bluetooth connection lost.", e);
setState(Sensor.SensorState.DISCONNECTED);
break;
Log.d(TAG, "Could not connect to bluetooth sensor: " + gatt.getDevice());
}
}
@Override
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
BluetoothGattCharacteristic characteristic = gatt
.getService(HEART_RATE_SERVICE_UUID)
.getCharacteristic(HEART_RATE_MEASUREMENT_CHAR_UUID);
gatt.setCharacteristicNotification(characteristic, true);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
//DOCUMENTATION https://www.bluetooth.com/specifications/gatt/characteristics/
byte[] raw = characteristic.getValue();
int index = ((raw[0] & 0x1) == 1) ? 2 : 1;
int format = (index == 1) ? BluetoothGattCharacteristic.FORMAT_UINT8 : BluetoothGattCharacteristic.FORMAT_UINT16;
int value = characteristic.getIntValue(format, index);
Log.d(TAG, "Received heart beat rate: " + value);
handler.obtainMessage(MESSAGE_READ, value, value).sendToTarget();
}
};
/**
* Constructor.
*
* @param handler a handler for sending messages back to the UI activity
*/
public BluetoothConnectionManager(Context context, Handler handler) {
this.context = context;
this.handler = handler;
this.sensorState = SensorState.NONE;
}
/**
* Cancels this thread.
* Gets the sensor state.
*/
public void cancel() {
try {
bluetoothSSocket.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close bluetooth socket.", e);
}
public synchronized SensorState getSensorState() {
return sensorState;
}
/**
* Sets the sensor state.
*
* @param sensorState the sensor state
*/
private synchronized void setState(Sensor.SensorState sensorState) {
this.sensorState = sensorState;
}
/**
* Resets the bluetooth connection manager.
*/
public synchronized void reset() {
//TODO Disconnect
setState(Sensor.SensorState.NONE);
}
/**
* Connects to a bluetooth device.
*
* @param bluetoothDevice the bluetooth device
*/
public synchronized void connect(BluetoothDevice bluetoothDevice) {
Log.d(TAG, "connect to: " + bluetoothDevice);
bluetoothDevice.connectGatt(this.context, false, this.connectCallback);
setState(Sensor.SensorState.CONNECTING);
}
}
}
@@ -16,6 +16,7 @@
package com.google.android.apps.mytracks.services.sensors;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Sensor.SensorState;
import com.google.android.apps.mytracks.util.PreferencesUtils;
@@ -25,6 +26,7 @@ import com.google.android.maps.mytracks.R;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -35,19 +37,17 @@ import java.util.ArrayList;
/**
* Bluetooth sensor manager.
*
*
* TODO: Handle a BluetoothGatt.STATE_DISCONNECTED
*
* @author Sandor Dornbush
*/
public class BluetoothSensorManager extends SensorManager {
// 1 second in milliseconds
private static final long ONE_SECOND = (long) UnitConversions.S_TO_MS;
private static final BluetoothAdapter bluetoothAdapter = getDefaultBluetoothAdapter();
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
/**
* Gets the default bluetooth adapter.
*/
private static BluetoothAdapter getDefaultBluetoothAdapter() {
// If from the main application thread, return directly
if (Thread.currentThread().equals(Looper.getMainLooper().getThread())) {
@@ -87,7 +87,6 @@ public class BluetoothSensorManager extends SensorManager {
}
private final Context context;
private final MessageParser messageParser;
private final BluetoothConnectionManager bluetoothConnectionManager;
private SensorDataSet sensorDataSet = null;
@@ -97,23 +96,14 @@ public class BluetoothSensorManager extends SensorManager {
public void handleMessage(Message message) {
switch (message.what) {
case BluetoothConnectionManager.MESSAGE_DEVICE_NAME:
String deviceName = message.getData()
.getString(BluetoothConnectionManager.KEY_DEVICE_NAME);
Toast.makeText(context, context.getString(R.string.settings_sensor_connected, deviceName),
Toast.LENGTH_SHORT).show();
String deviceName = message.getData().getString(BluetoothConnectionManager.KEY_DEVICE_NAME);
Toast.makeText(context, context.getString(R.string.settings_sensor_connected, deviceName), Toast.LENGTH_LONG).show();
break;
case BluetoothConnectionManager.MESSAGE_READ:
try {
byte[] readBuf = (byte[]) message.obj;
sensorDataSet = messageParser.parseBuffer(readBuf);
Log.d(TAG, "MESSAGE_READ: " + sensorDataSet);
} catch (IllegalArgumentException e) {
sensorDataSet = null;
Log.i(TAG, "Unexpected exception on read", e);
} catch (RuntimeException e) {
sensorDataSet = null;
Log.i(TAG, "Unexpected exception on read.", e);
}
sensorDataSet = SensorDataSet.newBuilder()
.setHeartRate(Sensor.SensorData.newBuilder().setValue(message.arg1)
.setState(SensorState.SENDING))
.build();
break;
default:
break;
@@ -122,16 +112,11 @@ public class BluetoothSensorManager extends SensorManager {
};
/**
* Constructor.
*
* @param context the context
* @param messageParser the message parser
*/
public BluetoothSensorManager(Context context, MessageParser messageParser) {
public BluetoothSensorManager(Context context) {
this.context = context;
this.messageParser = messageParser;
bluetoothConnectionManager = new BluetoothConnectionManager(
bluetoothAdapter, messageHandler, messageParser);
bluetoothConnectionManager = new BluetoothConnectionManager(context, messageHandler);
}
@Override
@@ -145,10 +130,9 @@ public class BluetoothSensorManager extends SensorManager {
Log.w(TAG, "Bluetooth not enabled.");
return;
}
String address = PreferencesUtils.getString(
context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
String address = PreferencesUtils.getString(context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
if (PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(address)) {
Log.w(TAG, "No blueooth address.");
Log.w(TAG, "No bluetooth address.");
return;
}
Log.w(TAG, "Connecting to bluetooth address: " + address);
@@ -1,35 +0,0 @@
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors;
import com.google.android.apps.mytracks.content.Sensor;
/**
* An interface for parsing a byte array to a SensorData object.
*
* @author Sandor Dornbush
*/
public interface MessageParser {
int getFrameSize();
Sensor.SensorDataSet parseBuffer(byte[] readBuff);
boolean isValid(byte[] buffer);
int findNextAlignment(byte[] buffer);
}
@@ -88,8 +88,6 @@ public class SensorManagerFactory {
* @param context the context
*/
private static SensorManager getSensorManager(Context context) {
//TODO Implement Bluetooth LE Sensor manager
return null;
return new BluetoothSensorManager(context);
}
}