forked from upstream-mirrors/OpenTracks
Implemented heart rate via Bluetooth LE.
This commit is contained in:
+106
-259
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-31
@@ -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);
|
||||
|
||||
-35
@@ -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);
|
||||
}
|
||||
+1
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user