Merge branch 'V3.4' into V3.5

This commit is contained in:
Graham Jones
2020-08-22 11:13:49 +01:00
19 changed files with 1643 additions and 720 deletions

View File

@@ -0,0 +1,323 @@
package uk.org.openseizuredetector;
/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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.
*/
import android.Manifest;
import android.app.Activity;
import android.app.ListActivity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.v4.app.ActivityCompat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
/**
* Activity for scanning and displaying available Bluetooth LE devices.
*/
public class BLEScanActivity extends ListActivity {
private LeDeviceListAdapter mLeDeviceListAdapter;
private BluetoothAdapter mBluetoothAdapter;
private boolean mScanning;
private Handler mHandler;
private boolean mPermissionsRequested = false;
private final String TAG = "BLEScanActivity";
private final String[] REQUIRED_PERMISSIONS = {
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
};
private static final int REQUEST_ENABLE_BT = 1;
// Stops scanning after 10 seconds.
private static final long SCAN_PERIOD = 10000;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//this.getActionBar().setTitle(R.string.title_devices);
this.setTitle(R.string.title_devices);
mHandler = new Handler();
// Use this check to determine whether BLE is supported on the device. Then you can
// selectively disable BLE-related features.
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
finish();
}
// Initializes a Bluetooth adapter. For API level 18 and above, get a reference to
// BluetoothAdapter through BluetoothManager.
final BluetoothManager bluetoothManager =
(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
// Checks if Bluetooth is supported on the device.
if (mBluetoothAdapter == null) {
Toast.makeText(this, R.string.error_bluetooth_not_supported, Toast.LENGTH_SHORT).show();
finish();
return;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.ble_scan_menu, menu);
if (!mScanning) {
menu.findItem(R.id.menu_stop).setVisible(false);
menu.findItem(R.id.menu_scan).setVisible(true);
menu.findItem(R.id.menu_refresh).setActionView(null);
} else {
menu.findItem(R.id.menu_stop).setVisible(true);
menu.findItem(R.id.menu_scan).setVisible(false);
menu.findItem(R.id.menu_refresh).setActionView(
R.layout.actionbar_indeterminate_progress);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_scan:
mLeDeviceListAdapter.clear();
scanLeDevice(true);
break;
case R.id.menu_stop:
scanLeDevice(false);
break;
}
return true;
}
@Override
protected void onResume() {
super.onResume();
// Ensures Bluetooth is enabled on the device. If Bluetooth is not currently enabled,
// fire an intent to display a dialog asking the user to grant permission to enable it.
if (!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
// Initializes list view adapter.
mLeDeviceListAdapter = new LeDeviceListAdapter();
setListAdapter(mLeDeviceListAdapter);
scanLeDevice(true);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// User chose not to enable Bluetooth.
if (requestCode == REQUEST_ENABLE_BT && resultCode == Activity.RESULT_CANCELED) {
finish();
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
protected void onPause() {
super.onPause();
scanLeDevice(false);
mLeDeviceListAdapter.clear();
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
final BluetoothDevice device = mLeDeviceListAdapter.getDevice(position);
if (device == null) return;
Log.v(TAG, "onListItemClick: Device=" + device.getName() + ", Addr=" + device.getAddress());
if (mScanning) {
mBluetoothAdapter.stopLeScan(mLeScanCallback);
mScanning = false;
}
Log.v(TAG,"Saving Device Details");
SharedPreferences.Editor SPE = PreferenceManager
.getDefaultSharedPreferences(this).edit();
try {
SPE.putString("BLE_Device_Addr", device.getAddress());
SPE.putString("BLE_Device_Name", device.getName());
SPE.apply();
SPE.commit();
Log.v(TAG, "Saved Device Name="+device.getName()+" and Address="+device.getAddress());
} catch (Exception ex) {
Log.e(TAG, "Error Saving Devie Name and Address!");
Toast toast = Toast.makeText(this, "Problem Saving Device Name and Address", Toast.LENGTH_SHORT);
toast.show();
}
SharedPreferences SP = PreferenceManager.getDefaultSharedPreferences((this));
Log.v(TAG,"Check of saved values - Name="+SP.getString("BLE_Device_Name","NOT SET")+", Addr="+SP.getString("BLE_Device_Addr","NOT SET"));
finish();
}
private void scanLeDevice(final boolean enable) {
requestPermissions(this);
if (enable) {
// Stops scanning after a pre-defined scan period.
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
invalidateOptionsMenu();
}
}, SCAN_PERIOD);
mScanning = true;
mBluetoothAdapter.startLeScan(mLeScanCallback);
} else {
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
invalidateOptionsMenu();
}
// Adapter for holding devices found through scanning.
private class LeDeviceListAdapter extends BaseAdapter {
private ArrayList<BluetoothDevice> mLeDevices;
private LayoutInflater mInflator;
public LeDeviceListAdapter() {
super();
mLeDevices = new ArrayList<BluetoothDevice>();
mInflator = BLEScanActivity.this.getLayoutInflater();
}
public void addDevice(BluetoothDevice device) {
if (!mLeDevices.contains(device)) {
Log.v(TAG,"addDevice - "+device.getName());
mLeDevices.add(device);
}
}
public BluetoothDevice getDevice(int position) {
return mLeDevices.get(position);
}
public void clear() {
mLeDevices.clear();
}
@Override
public int getCount() {
return mLeDevices.size();
}
@Override
public Object getItem(int i) {
return mLeDevices.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
ViewHolder viewHolder;
Log.v(TAG,"scanner getView i="+i);
// General ListView optimization code.
if (view == null) {
view = mInflator.inflate(R.layout.ble_list_item_device, null);
viewHolder = new ViewHolder();
viewHolder.deviceAddress = (TextView) view.findViewById(R.id.device_address);
viewHolder.deviceName = (TextView) view.findViewById(R.id.device_name);
view.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) view.getTag();
}
BluetoothDevice device = mLeDevices.get(i);
final String deviceName = device.getName();
if (deviceName != null && deviceName.length() > 0)
viewHolder.deviceName.setText(deviceName);
else
viewHolder.deviceName.setText(R.string.unknown_device);
viewHolder.deviceAddress.setText(device.getAddress());
return view;
}
}
// Device scan callback.
private BluetoothAdapter.LeScanCallback mLeScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
Log.v(TAG,"LEScanCallback - device="+device.getName());
runOnUiThread(new Runnable() {
@Override
public void run() {
mLeDeviceListAdapter.addDevice(device);
mLeDeviceListAdapter.notifyDataSetChanged();
}
});
}
};
static class ViewHolder {
TextView deviceName;
TextView deviceAddress;
}
public void requestPermissions(Activity activity) {
if (mPermissionsRequested) {
Log.i(TAG, "requestPermissions() - request already sent - not doing anything");
} else {
Log.i(TAG, "requestPermissions() - requesting permissions");
for (int i = 0; i < REQUIRED_PERMISSIONS.length; i++) {
if (ActivityCompat.shouldShowRequestPermissionRationale(activity,
REQUIRED_PERMISSIONS[i])) {
Log.i(TAG, "shouldShowRationale for permission" + REQUIRED_PERMISSIONS[i]);
}
}
ActivityCompat.requestPermissions(activity,
REQUIRED_PERMISSIONS,
42);
mPermissionsRequested = true;
}
}
}

View File

@@ -0,0 +1,24 @@
package uk.org.openseizuredetector;
// Defines the servies and characteristics we need to subscribe to.
import java.util.HashMap;
public class GattAttributes {
private static HashMap<String, String> attributes = new HashMap();
public static String HEART_RATE_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb";
public static String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb";
static {
// Sample Services.
attributes.put("0000180d-0000-1000-8000-00805f9b34fb", "Heart Rate Service");
attributes.put("0000180a-0000-1000-8000-00805f9b34fb", "Device Information Service");
// Sample Characteristics.
attributes.put("00002a37-0000-1000-8000-00805f9b34fb", "Heart Rate Measurement");
attributes.put("00002a29-0000-1000-8000-00805f9b34fb", "Manufacturer Name String");
}
public static String lookup(String uuid, String defaultName) {
String name = attributes.get(uuid);
return name == null ? defaultName : name;
}
}

View File

@@ -34,16 +34,19 @@ import android.os.Bundle;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import java.util.List;
public class PrefActivity extends PreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
public class PrefActivity extends PreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener, View.OnClickListener {
private String TAG = "PreferenceActivity";
private OsdUtil mUtil;
private boolean mPrefChanged = false;
private Context mContext;
private Handler mHandler;
private Button mSelectBLEButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -118,6 +121,8 @@ public class PrefActivity extends PreferenceActivity implements SharedPreference
}
}
//mSelectBLEButton = findViewById(R.id.selectBLEDeviceButton);
//mSelectBLEButton.setOnClickListener(this);
}
@@ -217,6 +222,20 @@ public class PrefActivity extends PreferenceActivity implements SharedPreference
return true;
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.selectBLEDeviceButton:
Log.v(TAG,"onClick - SelectBLEDeviceButton");
final Intent intent = new Intent(this.mContext, BLEScanActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
break;
default:
Log.e(TAG,"onClick - unrecognised button");
}
}
/**
* This fragment shows the preferences for the first header.
*/

View File

@@ -23,13 +23,26 @@
*/
package uk.org.openseizuredetector;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.text.format.Time;
import android.util.Log;
import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.jtransforms.fft.DoubleFFT_1D;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
interface SdDataReceiver {
public void onSdDataReceived(SdData sdData);
public void onSdDataFault(SdData sdData);
@@ -40,14 +53,56 @@ interface SdDataReceiver {
* network data source.
*/
public abstract class SdDataSource {
protected Handler mHandler = new Handler();
private Timer mStatusTimer;
private Timer mSettingsTimer;
private Timer mFaultCheckTimer;
protected Time mDataStatusTime;
protected boolean mWatchAppRunningCheck = false;
private int mAppRestartTimeout = 10; // Timeout before re-starting watch app (sec) if we have not received
// data after mDataUpdatePeriod
private int mFaultTimerPeriod = 30; // Fault Timer Period in sec
private int mSettingsPeriod = 60; // period between requesting settings in seconds.
public SdData mSdData;
public String mName = "undefined";
protected OsdUtil mUtil;
protected Context mContext;
protected Handler mHandler;
protected SdDataReceiver mSdDataReceiver;
private String TAG = "SdDataSource";
private short mDebug;
private short mFreqCutoff = 12;
private short mDisplaySpectrum;
private short mDataUpdatePeriod;
private short mMutePeriod;
private short mManAlarmPeriod;
private short mPebbleSdMode;
private short mSampleFreq;
private short mAlarmFreqMin;
private short mAlarmFreqMax;
private short mSamplePeriod;
private short mWarnTime;
private short mAlarmTime;
private short mAlarmThresh;
private short mAlarmRatioThresh;
private boolean mFallActive;
private short mFallThreshMin;
private short mFallThreshMax;
private short mFallWindow;
private int mMute; // !=0 means muted by keypress on watch.
// Values for SD_MODE
private int SIMPLE_SPEC_FMAX = 10;
private int ACCEL_SCALE_FACTOR = 1000; // Amount by which to reduce analysis results to scale to be comparable to analysis on Pebble.
private int mAlarmCount;
protected String mBleDeviceAddr;
protected String mBleDeviceName;
public SdDataSource(Context context, Handler handler, SdDataReceiver sdDataReceiver) {
Log.v(TAG, "SdDataSource() Constructor");
mContext = context;
@@ -70,7 +125,57 @@ public abstract class SdDataSource {
* make sure any changes to preferences are taken into account.
*/
public void start() {
Log.v(TAG, "start()");
updatePrefs();
// Start timer to check status of watch regularly.
mDataStatusTime = new Time(Time.getCurrentTimezone());
// use a timer to check the status of the pebble app on the same frequency
// as we get app data.
if (mStatusTimer == null) {
Log.v(TAG, "start(): starting status timer");
mUtil.writeToSysLogFile("SdDataSourceBLE.start() - starting status timer");
mStatusTimer = new Timer();
mStatusTimer.schedule(new TimerTask() {
@Override
public void run() {
getStatus();
}
}, 0, mDataUpdatePeriod * 1000);
} else {
Log.v(TAG, "start(): status timer already running.");
mUtil.writeToSysLogFile("SdDataSourceBLE.start() - status timer already running??");
}
if (mFaultCheckTimer == null) {
Log.v(TAG, "start(): starting alarm check timer");
mUtil.writeToSysLogFile("SdDataSourceBLE.start() - starting alarm check timer");
mFaultCheckTimer = new Timer();
mFaultCheckTimer.schedule(new TimerTask() {
@Override
public void run() {
faultCheck();
}
}, 0, 1000);
} else {
Log.v(TAG, "start(): alarm check timer already running.");
mUtil.writeToSysLogFile("SDDataSourceBLE.start() - alarm check timer already running??");
}
if (mSettingsTimer == null) {
Log.v(TAG, "start(): starting settings timer");
mUtil.writeToSysLogFile("SDDataSourceBLE.start() - starting settings timer");
mSettingsTimer = new Timer();
mSettingsTimer.schedule(new TimerTask() {
@Override
public void run() {
mSdData.haveSettings = false;
}
}, 0, 1000 * mSettingsPeriod); // ask for settings less frequently than we get data
} else {
Log.v(TAG, "start(): settings timer already running.");
mUtil.writeToSysLogFile("SDDataSourceBLE.start() - settings timer already running??");
}
}
/**
@@ -78,6 +183,37 @@ public abstract class SdDataSource {
*/
public void stop() {
Log.v(TAG, "stop()");
try {
// Stop the status timer
if (mStatusTimer != null) {
Log.v(TAG, "stop(): cancelling status timer");
mUtil.writeToSysLogFile("SDDataSourceBLE.stop() - cancelling status timer");
mStatusTimer.cancel();
mStatusTimer.purge();
mStatusTimer = null;
}
// Stop the settings timer
if (mSettingsTimer != null) {
Log.v(TAG, "stop(): cancelling settings timer");
mUtil.writeToSysLogFile("SDDataSourceBLE.stop() - cancelling settings timer");
mSettingsTimer.cancel();
mSettingsTimer.purge();
mSettingsTimer = null;
}
// Stop the alarm check timer
if (mFaultCheckTimer != null) {
Log.v(TAG, "stop(): cancelling alarm check timer");
mUtil.writeToSysLogFile("SDDataSourceBLE.stop() - cancelling alarm check timer");
mFaultCheckTimer.cancel();
mFaultCheckTimer.purge();
mFaultCheckTimer = null;
}
} catch (Exception e) {
Log.v(TAG, "Error in stop() - " + e.toString());
mUtil.writeToSysLogFile("SDDataSourceBLE.stop() - error - "+e.toString());
}
}
/**
@@ -101,14 +237,539 @@ public abstract class SdDataSource {
public void acceptAlarm() { Log.v(TAG,"acceptAlarm()"); }
// Force the data stored in this datasource to update in line with the JSON string encoded data provided.
// Used by webServer to update the NetworkPassiveDatasource
// Used by webServer to update the GarminDatasource.
// Returns a message string that is passed back to the watch.
public String updateFromJSON(String jsonStr) {
String retVal = "undefined";
String watchPartNo;
String watchFwVersion;
String sdVersion;
String sdName;
Log.v(TAG,"updateFromJSON - "+jsonStr);
return("OK");
try {
JSONObject mainObject = new JSONObject(jsonStr);
//JSONObject dataObject = mainObject.getJSONObject("dataObj");
JSONObject dataObject = mainObject;
String dataTypeStr = dataObject.getString("dataType");
Log.v(TAG,"updateFromJSON - dataType="+dataTypeStr);
if (dataTypeStr.equals("raw")) {
Log.v(TAG,"updateFromJSON - processing raw data");
try {
mSdData.mHR = dataObject.getDouble("HR");
} catch (JSONException e) {
// if we get 'null' HR (For example if the heart rate is not working)
mSdData.mHR = -1;
}
try {
mMute = dataObject.getInt("Mute");
} catch (JSONException e) {
// if we get 'null' HR (For example if the heart rate is not working)
mMute = 0;
}
JSONArray accelVals = dataObject.getJSONArray("data");
Log.v(TAG, "Received " + accelVals.length() + " acceleration values");
int i;
for (i = 0; i < accelVals.length(); i++) {
mSdData.rawData[i] = accelVals.getInt(i);
}
mSdData.mNsamp = accelVals.length();
//mNSamp = accelVals.length();
mWatchAppRunningCheck = true;
doAnalysis();
if (mSdData.haveSettings == false) {
retVal = "sendSettings";
} else {
retVal = "OK";
}
} else if (dataTypeStr.equals("settings")){
Log.v(TAG,"updateFromJSON - processing settings");
mSamplePeriod = (short)dataObject.getInt("analysisPeriod");
mSampleFreq = (short)dataObject.getInt("sampleFreq");
mSdData.batteryPc = (short)dataObject.getInt("battery");
Log.v(TAG,"updateFromJSON - mSamplePeriod="+mSamplePeriod+" mSampleFreq="+mSampleFreq);
mUtil.writeToSysLogFile("SDDataSourceBLE.updateFromJSON - Settings Received");
mUtil.writeToSysLogFile(" * mSamplePeriod="+mSamplePeriod+" mSampleFreq="+mSampleFreq);
mUtil.writeToSysLogFile(" * batteryPc = "+mSdData.batteryPc);
try {
watchPartNo = dataObject.getString("watchPartNo");
watchFwVersion = dataObject.getString("watchFwVersion");
sdVersion = dataObject.getString("sdVersion");
sdName = dataObject.getString("sdName");
mUtil.writeToSysLogFile(" * sdName = "+sdName+" version "+sdVersion);
mUtil.writeToSysLogFile(" * watchPartNo = "+watchPartNo+" fwVersion "+watchFwVersion);
} catch (Exception e) {
Log.e(TAG,"updateFromJSON - Error Parsing V3.2 JSON String - "+e.toString());
mUtil.writeToSysLogFile("updateFromJSON - Error Parsing V3.2 JSON String - "+e.toString());
mUtil.writeToSysLogFile(" This is probably because of an out of date watch app - please upgrade!");
e.printStackTrace();
}
mSdData.haveSettings = true;
mSdData.mSampleFreq = mSampleFreq;
mWatchAppRunningCheck = true;
retVal = "OK";
} else {
Log.e(TAG,"updateFromJSON - unrecognised dataType "+dataTypeStr);
retVal = "ERROR";
}
} catch (Exception e) {
Log.e(TAG,"updateFromJSON - Error Parsing JSON String - "+e.toString());
mUtil.writeToSysLogFile("updateFromJSON - Error Parsing JSON String - "+e.toString());
e.printStackTrace();
retVal = "ERROR";
}
return(retVal);
}
/**
* Calculate the magnitude of entry i in the fft array fft
* @param fft
* @param i
* @return magnitude ( Re*Re + Im*Im )
*/
private double getMagnitude(double[] fft, int i) {
double mag;
mag = (fft[2*i]*fft[2*i] + fft[2*i + 1] * fft[2*i +1]);
return mag;
}
/**
* doAnalysis() - analyse the data if the accelerometer data array mAccData
* and populate the output data structure mSdData
*/
protected void doAnalysis() {
// FIXME - Use specified sampleFreq, not this hard coded one
mSampleFreq = 25;
double freqRes = 1.0*mSampleFreq/mSdData.mNsamp;
Log.v(TAG,"doAnalysis(): mSampleFreq="+mSampleFreq+" mNSamp="+mSdData.mNsamp+": freqRes="+freqRes);
// Set the frequency bounds for the analysis in fft output bin numbers.
int nMin = (int)(mAlarmFreqMin/freqRes);
int nMax = (int)(mAlarmFreqMax /freqRes);
Log.v(TAG,"doAnalysis(): mAlarmFreqMin="+mAlarmFreqMin+", nMin="+nMin
+", mAlarmFreqMax="+mAlarmFreqMax+", nMax="+nMax);
// Calculate the bin number of the cutoff frequency
int nFreqCutoff = (int)(mFreqCutoff /freqRes);
Log.v(TAG,"mFreqCutoff = "+mFreqCutoff+", nFreqCutoff="+nFreqCutoff);
DoubleFFT_1D fftDo = new DoubleFFT_1D(mSdData.mNsamp);
double[] fft = new double[mSdData.mNsamp * 2];
///System.arraycopy(mAccData, 0, fft, 0, mNsamp);
System.arraycopy(mSdData.rawData, 0, fft, 0, mSdData.mNsamp);
fftDo.realForward(fft);
// Calculate the whole spectrum power (well a value equivalent to it that avoids square root calculations
// and zero any readings that are above the frequency cutoff.
double specPower = 0;
for (int i = 1; i < mSdData.mNsamp / 2; i++) {
if (i <= nFreqCutoff) {
specPower = specPower + getMagnitude(fft,i);
} else {
fft[2*i] = 0.;
fft[2*i+1] = 0.;
}
}
//Log.v(TAG,"specPower = "+specPower);
//specPower = specPower/(mSdData.mNsamp/2);
specPower = specPower/mSdData.mNsamp/2;
//Log.v(TAG,"specPower = "+specPower);
// Calculate the Region of Interest power and power ratio.
double roiPower = 0;
for (int i=nMin;i<nMax;i++) {
roiPower = roiPower + getMagnitude(fft,i);
}
roiPower = roiPower/(nMax - nMin);
double roiRatio = 10 * roiPower / specPower;
// Calculate the simplified spectrum - power in 1Hz bins.
double[] simpleSpec = new double[SIMPLE_SPEC_FMAX+1];
for (int ifreq=0;ifreq<SIMPLE_SPEC_FMAX;ifreq++) {
int binMin = (int)(1 + ifreq/freqRes); // add 1 to loose dc component
int binMax = (int)(1 + (ifreq+1)/freqRes);
simpleSpec[ifreq]=0;
for (int i=binMin;i<binMax;i++) {
simpleSpec[ifreq] = simpleSpec[ifreq] + getMagnitude(fft,i);
}
simpleSpec[ifreq] = simpleSpec[ifreq] / (binMax-binMin);
}
// Populate the mSdData structure to communicate with the main SdServer service.
mDataStatusTime.setToNow();
mSdData.specPower = (long)specPower / ACCEL_SCALE_FACTOR;
mSdData.roiPower = (long)roiPower / ACCEL_SCALE_FACTOR;
mSdData.dataTime.setToNow();
mSdData.maxVal = 0; // not used
mSdData.maxFreq = 0; // not used
mSdData.haveData = true;
mSdData.alarmThresh = mAlarmThresh;
mSdData.alarmRatioThresh = mAlarmRatioThresh;
mSdData.alarmFreqMin = mAlarmFreqMin;
mSdData.alarmFreqMax = mAlarmFreqMax;
// note mSdData.batteryPc is set from settings data in updateFromJSON()
// FIXME - I haven't worked out why dividing by 1000 seems necessary to get the graph on scale - we don't seem to do that with the Pebble.
for(int i=0;i<SIMPLE_SPEC_FMAX;i++) {
mSdData.simpleSpec[i] = (int)simpleSpec[i]/ACCEL_SCALE_FACTOR;
}
Log.v(TAG, "simpleSpec = " + Arrays.toString(mSdData.simpleSpec));
// Because we have received data, set flag to show watch app running.
mWatchAppRunningCheck = true;
// Check this data to see if it represents an alarm state.
alarmCheck();
hrCheck();
fallCheck();
muteCheck();
mSdDataReceiver.onSdDataReceived(mSdData); // and tell SdServer we have received data.
}
/****************************************************************
* checkAlarm() - checks the current accelerometer data and uses
* historical data to determine if we are in a fault, warning or ok
* state.
* Sets mSdData.alarmState and mSdData.hrAlarmStanding
*/
private void alarmCheck() {
boolean inAlarm;
Log.v(TAG, "alarmCheck()");
// Is the current set of data representing an alarm state?
if ((mSdData.roiPower > mAlarmThresh) && (10 * (mSdData.roiPower / mSdData.specPower) > mAlarmRatioThresh)) {
inAlarm = true;
} else {
inAlarm = false;
}
// set the alarmState to Alarm, Warning or OK, depending on the current state and previous ones.
if (inAlarm) {
mAlarmCount += mSamplePeriod;
if (mAlarmCount > mAlarmTime) {
// full alarm
mSdData.alarmState = 2;
} else if (mAlarmCount > mWarnTime) {
// warning
mSdData.alarmState = 1;
}
} else {
// If we are not in an ALARM state, revert back to WARNING, otherwise
// revert back to OK.
if (mSdData.alarmState == 2) {
// revert to warning
mSdData.alarmState = 1;
mAlarmCount = mWarnTime + 1; // pretend we have only just entered warning state.
} else {
// revert to OK
mSdData.alarmState = 0;
mAlarmCount = 0;
}
}
Log.v(TAG, "alarmCheck(): inAlarm=" + inAlarm + ", alarmState = " + mSdData.alarmState + " alarmCount=" + mAlarmCount + " mAlarmTime=" + mAlarmTime);
}
public void muteCheck() {
if (mMute != 0) {
Log.v(TAG, "Mute Active - setting alarms to mute");
mSdData.alarmState = 6;
mSdData.alarmPhrase = "MUTE";
mSdData.mHRAlarmStanding = false;
}
}
/**
* hrCheck - check the Heart rate data in mSdData to see if it represents an alarm condition.
* Sets mSdData.mHRAlarmStanding
*/
public void hrCheck() {
Log.v(TAG, "hrCheck()");
/* Check Heart Rate against alarm settings */
if (mSdData.mHRAlarmActive) {
if (mSdData.mHR < 0) {
if (mSdData.mHRNullAsAlarm) {
Log.i(TAG, "Heart Rate Null - Alarming");
mSdData.mHRFaultStanding = false;
mSdData.mHRAlarmStanding = true;
} else {
Log.i(TAG, "Heart Rate Fault (HR<0)");
mSdData.mHRFaultStanding = true;
mSdData.mHRAlarmStanding = false;
}
}
else if ((mSdData.mHR > mSdData.mHRThreshMax) || (mSdData.mHR < mSdData.mHRThreshMin)) {
Log.i(TAG, "Heart Rate Abnormal - " + mSdData.mHR + " bpm");
mSdData.mHRFaultStanding = false;
mSdData.mHRAlarmStanding = true;
}
else {
mSdData.mHRFaultStanding = false;
mSdData.mHRAlarmStanding = false;
}
}
}
/****************************************************************
* Simple threshold analysis to chech for fall.
* Called from clock_tick_handler()
*/
public void fallCheck() {
int i,j;
double minAcc, maxAcc;
long fallWindowSamp = (mFallWindow*mSdData.mSampleFreq)/1000; // Convert ms to samples.
Log.v(TAG, "check_fall() - fallWindowSamp=" +fallWindowSamp);
// Move window through sample buffer, checking for fall.
// Note - not resetting fallAlarmStanding means that fall alarms will always latch until the 'Accept Alarm' button
// is pressed.
//mSdData.fallAlarmStanding = false;
if (mFallActive) {
mSdData.mFallActive = true;
for (i = 0; i < mSdData.mNsamp - fallWindowSamp; i++) { // i = window start point
// Find max and min acceleration within window.
minAcc = mSdData.rawData[i];
maxAcc = mSdData.rawData[i];
for (j = 0; j < fallWindowSamp; j++) { // j = position within window
if (mSdData.rawData[i + j] < minAcc) minAcc = mSdData.rawData[i + j];
if (mSdData.rawData[i + j] > maxAcc) maxAcc = mSdData.rawData[i + j];
}
if ((minAcc < mFallThreshMin) && (maxAcc > mFallThreshMax)) {
Log.d(TAG, "check_fall() - minAcc=" + minAcc + ", maxAcc=" + maxAcc);
Log.d(TAG, "check_fall() - ****FALL DETECTED****");
mSdData.fallAlarmStanding = true;
return;
}
if (mMute != 0) {
Log.v(TAG,"Mute Active - setting fall alarm to mute");
mSdData.fallAlarmStanding = false;
}
}
} else {
mSdData.mFallActive = false;
Log.v(TAG,"check_fall - mFallActive is false - doing nothing");
}
//if (debug) APP_LOG(APP_LOG_LEVEL_DEBUG,"check_fall() - minAcc=%d, maxAcc=%d",
// minAcc,maxAcc);
}
/**
* Checks the status of the connection to the watch,
* and sets class variables for use by other functions.
*/
public void getStatus() {
Time tnow = new Time(Time.getCurrentTimezone());
long tdiff;
tnow.setToNow();
// get time since the last data was received from the Pebble watch.
tdiff = (tnow.toMillis(false) - mDataStatusTime.toMillis(false));
Log.v(TAG, "getStatus() - mWatchAppRunningCheck=" + mWatchAppRunningCheck + " tdiff=" + tdiff);
Log.v(TAG,"getStatus() - tdiff="+tdiff+", mDataUpatePeriod="+mDataUpdatePeriod+", mAppRestartTimeout="+mAppRestartTimeout);
mSdData.watchConnected = true; // We can't check connection for passive network connection, so set it to true to avoid errors.
// And is the watch app running?
// set mWatchAppRunningCheck has been false for more than 10 seconds
// the app is not talking to us
// mWatchAppRunningCheck is set to true in the receiveData handler.
if (!mWatchAppRunningCheck &&
(tdiff > (mDataUpdatePeriod + mAppRestartTimeout) * 1000)) {
Log.v(TAG, "getStatus() - tdiff = " + tdiff);
mSdData.watchAppRunning = false;
// Only make audible warning beep if we have not received data for more than mFaultTimerPeriod seconds.
if (tdiff > (mDataUpdatePeriod + mFaultTimerPeriod) * 1000) {
Log.v(TAG, "getStatus() - Watch App Not Running");
mUtil.writeToSysLogFile("SDDataSourceBLE.getStatus() - Watch App not Running");
//mDataStatusTime.setToNow();
mSdData.roiPower = -1;
mSdData.specPower = -1;
mSdDataReceiver.onSdDataFault(mSdData);
} else {
Log.v(TAG, "getStatus() - Waiting for mFaultTimerPeriod before issuing audible warning...");
}
} else {
mSdData.watchAppRunning = true;
}
// if we have confirmation that the app is running, reset the
// status time to now and initiate another check.
if (mWatchAppRunningCheck) {
mWatchAppRunningCheck = false;
mDataStatusTime.setToNow();
}
if (!mSdData.haveSettings) {
Log.v(TAG, "getStatus() - no settings received yet");
}
}
/**
* faultCheck - determines alarm state based on seizure detector data SdData. Called every second.
*/
private void faultCheck() {
Time tnow = new Time(Time.getCurrentTimezone());
long tdiff;
tnow.setToNow();
// get time since the last data was received from the watch.
tdiff = (tnow.toMillis(false) - mDataStatusTime.toMillis(false));
Log.v(TAG, "faultCheck() - tdiff=" + tdiff + ", mDataUpatePeriod=" + mDataUpdatePeriod + ", mAppRestartTimeout=" + mAppRestartTimeout
+ ", combined = " + (mDataUpdatePeriod + mAppRestartTimeout) * 1000);
if (!mWatchAppRunningCheck &&
(tdiff > (mDataUpdatePeriod + mAppRestartTimeout) * 1000)) {
Log.v(TAG, "faultCheck() - watch app not running so not doing anything");
mAlarmCount = 0;
}
}
/**
* updatePrefs() - update basic settings from the SharedPreferences
* - defined in res/xml/SdDataSourceNetworkPassivePrefs.xml
*/
public void updatePrefs() {
Log.v(TAG, "updatePrefs()");
mUtil.writeToSysLogFile("SDDataSourceBLE.updatePrefs()");
SharedPreferences SP = PreferenceManager
.getDefaultSharedPreferences(mContext);
try {
// Parse the AppRestartTimeout period setting.
try {
String appRestartTimeoutStr = SP.getString("AppRestartTimeout", "10");
mAppRestartTimeout = Integer.parseInt(appRestartTimeoutStr);
Log.v(TAG, "updatePrefs() - mAppRestartTimeout = " + mAppRestartTimeout);
} catch (Exception ex) {
Log.v(TAG, "updatePrefs() - Problem with AppRestartTimeout preference!");
Toast toast = Toast.makeText(mContext, "Problem Parsing AppRestartTimeout Preference", Toast.LENGTH_SHORT);
toast.show();
}
// Parse the FaultTimer period setting.
try {
String faultTimerPeriodStr = SP.getString("FaultTimerPeriod", "30");
mFaultTimerPeriod = Integer.parseInt(faultTimerPeriodStr);
Log.v(TAG, "updatePrefs() - mFaultTimerPeriod = " + mFaultTimerPeriod);
} catch (Exception ex) {
Log.v(TAG, "updatePrefs() - Problem with FaultTimerPeriod preference!");
Toast toast = Toast.makeText(mContext, "Problem Parsing FaultTimerPeriod Preference", Toast.LENGTH_SHORT);
toast.show();
}
// Watch Settings
String prefStr;
prefStr = SP.getString("BLE_Device_Addr", "SET_FROM_XML");
mBleDeviceAddr = prefStr;
Log.v(TAG,"mBLEDeviceAddr="+mBleDeviceAddr);
prefStr = SP.getString("BLE_Device_Name", "SET_FROM_XML");
mBleDeviceName = prefStr;
Log.v(TAG,"mBLEDeviceName="+mBleDeviceName);
prefStr = SP.getString("PebbleDebug", "SET_FROM_XML");
if (prefStr != null) {
mDebug = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() Debug = " + mDebug);
prefStr = SP.getString("PebbleDisplaySpectrum", "SET_FROM_XML");
mDisplaySpectrum = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() DisplaySpectrum = " + mDisplaySpectrum);
prefStr = SP.getString("PebbleUpdatePeriod", "SET_FROM_XML");
mDataUpdatePeriod = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() DataUpdatePeriod = " + mDataUpdatePeriod);
prefStr = SP.getString("MutePeriod", "SET_FROM_XML");
mMutePeriod = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() MutePeriod = " + mMutePeriod);
prefStr = SP.getString("ManAlarmPeriod", "SET_FROM_XML");
mManAlarmPeriod = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() ManAlarmPeriod = " + mManAlarmPeriod);
prefStr = SP.getString("PebbleSdMode", "SET_FROM_XML");
mPebbleSdMode = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() PebbleSdMode = " + mPebbleSdMode);
prefStr = SP.getString("SampleFreq", "SET_FROM_XML");
mSampleFreq = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() SampleFreq = " + mSampleFreq);
prefStr = SP.getString("SamplePeriod", "SET_FROM_XML");
mSamplePeriod = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() AnalysisPeriod = " + mSamplePeriod);
prefStr = SP.getString("AlarmFreqMin", "SET_FROM_XML");
mAlarmFreqMin = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() AlarmFreqMin = " + mAlarmFreqMin);
prefStr = SP.getString("AlarmFreqMax", "SET_FROM_XML");
mAlarmFreqMax = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() AlarmFreqMax = " + mAlarmFreqMax);
prefStr = SP.getString("WarnTime", "SET_FROM_XML");
mWarnTime = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() WarnTime = " + mWarnTime);
prefStr = SP.getString("AlarmTime", "SET_FROM_XML");
mAlarmTime = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() AlarmTime = " + mAlarmTime);
prefStr = SP.getString("AlarmThresh", "SET_FROM_XML");
mAlarmThresh = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() AlarmThresh = " + mAlarmThresh);
prefStr = SP.getString("AlarmRatioThresh", "SET_FROM_XML");
mAlarmRatioThresh = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() AlarmRatioThresh = " + mAlarmRatioThresh);
mFallActive = SP.getBoolean("FallActive", false);
Log.v(TAG, "updatePrefs() FallActive = " + mFallActive);
prefStr = SP.getString("FallThreshMin", "SET_FROM_XML");
mFallThreshMin = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() FallThreshMin = " + mFallThreshMin);
prefStr = SP.getString("FallThreshMax", "SET_FROM_XML");
mFallThreshMax = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() FallThreshMax = " + mFallThreshMax);
prefStr = SP.getString("FallWindow", "SET_FROM_XML");
mFallWindow = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() FallWindow = " + mFallWindow);
mSdData.mHRAlarmActive = SP.getBoolean("HRAlarmActive", false);
Log.v(TAG, "updatePrefs() HRAlarmActive = " + mSdData.mHRAlarmActive);
mSdData.mHRNullAsAlarm = SP.getBoolean("HRNullAsAlarm", false);
Log.v(TAG, "updatePrefs() HRNullAsAlarm = " + mSdData.mHRNullAsAlarm);
prefStr = SP.getString("HRThreshMin", "SET_FROM_XML");
mSdData.mHRThreshMin = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() HRThreshMin = " + mSdData.mHRThreshMin);
prefStr = SP.getString("HRThreshMax", "SET_FROM_XML");
mSdData.mHRThreshMax = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() HRThreshMax = " + mSdData.mHRThreshMax);
} else {
Log.v(TAG, "updatePrefs() - prefStr is null - WHY????");
mUtil.writeToSysLogFile("SDDataSourceBLE.updatePrefs() - prefStr is null - WHY??");
Toast toast = Toast.makeText(mContext, "Problem Parsing Preferences - Something won't work - Please go back to Settings and correct it!", Toast.LENGTH_SHORT);
toast.show();
}
} catch (Exception ex) {
Log.v(TAG, "updatePrefs() - Problem parsing preferences!");
mUtil.writeToSysLogFile("SDDataSourceBLE.updatePrefs() - ERROR "+ex.toString());
Toast toast = Toast.makeText(mContext, "Problem Parsing Preferences - Something won't work - Please go back to Settings and correct it!", Toast.LENGTH_SHORT);
toast.show();
}
}
/**
* Display a Toast message on screen.
* @param msg - message to display.
@@ -119,4 +780,18 @@ public abstract class SdDataSource {
}
public class SdDataBroadcastReceiver extends BroadcastReceiver {
//private String TAG = "SdDataBroadcastReceiver";
@Override
public void onReceive(Context context, Intent intent) {
Log.v(TAG,"SdDataBroadcastReceiver.onReceive()");
String jsonStr = intent.getStringExtra("data");
Log.v(TAG,"SdDataBroadcastReceiver.onReceive() - data="+jsonStr);
updateFromJSON(jsonStr);
}
}
}

View File

@@ -0,0 +1,398 @@
/*
Android_Pebble_sd - Android alarm client for openseizuredetector..
See http://openseizuredetector.org for more information.
Copyright Graham Jones, 2015, 2016
This file is part of pebble_sd.
Android_Pebble_sd is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Android_Pebble_sd 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with Android_pebble_sd. If not, see <http://www.gnu.org/licenses/>.
*/
package uk.org.openseizuredetector;
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.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.text.format.Time;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
/**
* A data source that registers for BLE GATT notifications from a device and
* waits to be notified of data being available.
*/
public class SdDataSourceBLE extends SdDataSource {
private int MAX_RAW_DATA = 125; // 5 seconds at 25 Hz.
private String TAG = "SdDataSourceBLE";
private BluetoothManager mBluetoothManager;
private BluetoothAdapter mBluetoothAdapter;
private String mBluetoothDeviceAddress;
private BluetoothGatt mBluetoothGatt;
private int mConnectionState = STATE_DISCONNECTED;
private int nRawData = 0;
private double[] rawData = new double[MAX_RAW_DATA];
private static final int STATE_DISCONNECTED = 0;
private static final int STATE_CONNECTING = 1;
private static final int STATE_CONNECTED = 2;
public final static String ACTION_GATT_CONNECTED =
"com.example.bluetooth.le.ACTION_GATT_CONNECTED";
public final static String ACTION_GATT_DISCONNECTED =
"com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";
public final static String ACTION_GATT_SERVICES_DISCOVERED =
"com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";
public final static String ACTION_DATA_AVAILABLE =
"com.example.bluetooth.le.ACTION_DATA_AVAILABLE";
public final static String EXTRA_DATA =
"com.example.bluetooth.le.EXTRA_DATA";
public static String SERV_DEV_INFO = "0000180a-0000-1000-8000-00805f9b34fb";
public static String SERV_HEART_RATE = "0000180d-0000-1000-8000-00805f9b34fb";
public static String SERV_OSD = "a19585e9-0001-39d0-015f-b3e2b9a0c854";
public static String CHAR_HEART_RATE_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb";
public static String CHAR_MANUF_NAME = "00002a29-0000-1000-8000-00805f9b34fb";
public static String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb";
public static String CHAR_OSD_ACC_DATA = "a19585e9-0002-39d0-015f-b3e2b9a0c854";
public static String CHAR_OSD_BATT_DATA = "a19585e9-0004-39d0-015f-b3e2b9a0c854";
public final static UUID UUID_HEART_RATE_MEASUREMENT = UUID.fromString(CHAR_HEART_RATE_MEASUREMENT);
private BluetoothGatt mGatt;
private BluetoothGattCharacteristic mBattChar;
public SdDataSourceBLE(Context context, Handler handler,
SdDataReceiver sdDataReceiver) {
super(context, handler, sdDataReceiver);
mName = "BLE";
// Set default settings from XML files (mContext is set by super().
PreferenceManager.setDefaultValues(mContext,
R.xml.network_passive_datasource_prefs, true);
}
/**
* Start the datasource updating - initialises from sharedpreferences first to
* make sure any changes to preferences are taken into account.
*/
public void start() {
Log.i(TAG, "start()");
super.start();
mUtil.writeToSysLogFile("SdDataSourceBLE.start() - mBleDeviceAddr=" + mBleDeviceAddr);
if (mBleDeviceAddr == "" || mBleDeviceAddr == null) {
final Intent intent = new Intent(this.mContext, BLEScanActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
Log.i(TAG, "mBLEDevice is " + mBleDeviceName + ", Addr=" + mBleDeviceAddr);
bleConnect();
}
private void bleConnect() {
mSdData.watchConnected = false;
mSdData.watchAppRunning = false;
mBluetoothGatt = null;
mConnectionState = STATE_DISCONNECTED;
if (mBluetoothManager == null) {
mBluetoothManager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
if (mBluetoothManager == null) {
Log.e(TAG, "bleConnect(): Unable to initialize BluetoothManager.");
return;
}
}
mBluetoothAdapter = mBluetoothManager.getAdapter();
if (mBluetoothAdapter == null) {
Log.e(TAG, "bleConnect(): Unable to obtain a BluetoothAdapter.");
return;
}
if (mBluetoothAdapter == null || mBleDeviceAddr == null) {
Log.w(TAG, "bleConnect(): BluetoothAdapter not initialized or unspecified address.");
return;
}
final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(mBleDeviceAddr);
if (device == null) {
Log.w(TAG, "bleConnect(): Device not found. Unable to connect.");
return;
} else {
// We want to directly connect to the device, so we are setting the autoConnect
// parameter to false.
mBluetoothGatt = device.connectGatt(mContext, true, mGattCallback);
Log.d(TAG, "bleConnect(): Trying to create a new connection.");
mBluetoothDeviceAddress = mBleDeviceAddr;
mConnectionState = STATE_CONNECTING;
}
}
private void bleDisconnect() {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter not initialized");
return;
}
mBluetoothGatt.disconnect();
if (mBluetoothGatt == null) {
return;
}
mBluetoothGatt.close();
mBluetoothGatt = null;
mSdData.watchAppRunning = false;
mSdData.watchConnected = false;
mConnectionState = STATE_DISCONNECTED;
}
/**
* Stop the datasource from updating
*/
public void stop() {
Log.i(TAG, "stop()");
mUtil.writeToSysLogFile("SDDataSourceBLE.stop()");
bleDisconnect();
super.stop();
}
// Implements callback methods for GATT events that the app cares about. For example,
// connection change and services discovered.
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
mConnectionState = STATE_CONNECTED;
mSdData.watchConnected = true;
Log.i(TAG, "onConnectionStateChange(): Connected to GATT server.");
// Attempts to discover services after successful connection.
Log.i(TAG, "onConnectionStateChange(): Attempting to start service discovery:");
mBluetoothGatt.discoverServices();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
mConnectionState = STATE_DISCONNECTED;
mSdData.watchConnected = false;
Log.i(TAG, "onConnectionStateChange(): Disconnected from GATT server - reconnecting after delay...");
//bleDisconnect(); // Tidy up connections
// Wait 2 seconds to give the server chance to shutdown, then re-start it
mHandler.postDelayed(new Runnable() {
public void run() {
bleConnect();
}
}, 2000);
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
boolean foundOsdService = false;
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.v(TAG, "Services discovered");
List<BluetoothGattService> serviceList = mBluetoothGatt.getServices();
for (int i = 0; i < serviceList.size(); i++) {
String uuidStr = serviceList.get(i).getUuid().toString();
Log.v(TAG, "Service " + uuidStr);
List<BluetoothGattCharacteristic> gattCharacteristics =
serviceList.get(i).getCharacteristics();
if (uuidStr.equals(SERV_DEV_INFO)) {
Log.v(TAG, "Device Info Service Discovered");
} else if (uuidStr.equals(SERV_HEART_RATE)) {
Log.v(TAG, "Heart Rate Service Discovered");
for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {
String charUuidStr = gattCharacteristic.getUuid().toString();
if (charUuidStr.equals(CHAR_HEART_RATE_MEASUREMENT)) {
Log.v(TAG, "Subscribing to Heart Rate Measurement Change Notifications");
setCharacteristicNotification(gattCharacteristic, true);
}
}
} else if (uuidStr.equals(SERV_OSD)) {
Log.v(TAG, "OpenSeizureDetector Service Discovered");
foundOsdService = true;
for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {
String charUuidStr = gattCharacteristic.getUuid().toString();
if (charUuidStr.equals(CHAR_OSD_ACC_DATA)) {
Log.v(TAG, "Subscribing to Acceleration Data Change Notifications");
setCharacteristicNotification(gattCharacteristic,true);
}
else if (charUuidStr.equals(CHAR_OSD_BATT_DATA)) {
Log.v(TAG,"Saving battery characteristic for later");
mBattChar = gattCharacteristic;
}
}
}
}
if (foundOsdService) {
mGatt = gatt;
} else {
Log.v(TAG, "device is not offering the OSD Gatt Service - re-trying connection");
bleDisconnect();
// Wait 1 second to give the server chance to shutdown, then re-start it
mHandler.postDelayed(new Runnable() {
public void run() {
bleConnect();
}
}, 1000);
}
} else {
Log.w(TAG, "onServicesDiscovered received: " + status);
}
}
public void onDataReceived(BluetoothGattCharacteristic characteristic) {
// FIXME - collect data until we have enough to do analysis, then use onDataReceived to process it.
//Log.v(TAG,"onDataReceived: Characteristic="+characteristic.getUuid().toString());
if (characteristic.getUuid().toString().equals(CHAR_HEART_RATE_MEASUREMENT)) {
int flag = characteristic.getProperties();
int format = -1;
if ((flag & 0x01) != 0) {
format = BluetoothGattCharacteristic.FORMAT_UINT16;
//Log.d(TAG, "Heart rate format UINT16.");
} else {
format = BluetoothGattCharacteristic.FORMAT_UINT8;
//Log.d(TAG, "Heart rate format UINT8.");
}
final int heartRate = characteristic.getIntValue(format, 1);
Log.d(TAG, String.format("Received heart rate: %d", heartRate));
}
else if (characteristic.getUuid().toString().equals(CHAR_OSD_ACC_DATA)) {
//Log.v(TAG,"Received OSD ACC DATA"+characteristic.getValue());
byte[] rawDataBytes = characteristic.getValue();
Log.v(TAG, "CHAR_OSD_ACC_DATA: numSamples = " + rawDataBytes.length+" nRawData="+nRawData);
for (int i = 0; i < rawDataBytes.length;i++) {
if (nRawData < MAX_RAW_DATA) {
rawData[nRawData] = 1000 * rawDataBytes[i] / 64; // Scale to mg
nRawData++;
} else {
Log.i(TAG, "RawData Buffer Full - processing data");
// Re-start collecting raw data.
mSdData.watchAppRunning = true;
for (i = 0; i < rawData.length; i++) {
mSdData.rawData[i] = rawData[i];
}
mSdData.mNsamp = rawData.length;
//mNSamp = accelVals.length();
mWatchAppRunningCheck = true;
mDataStatusTime = new Time(Time.getCurrentTimezone());
if (mSdData.haveSettings == false) {
Log.v(TAG,"Requesting Battery Data");
mGatt.readCharacteristic(mBattChar);
}
doAnalysis();
nRawData = 0;
}
}
}
else if (characteristic.getUuid().toString().equals(CHAR_OSD_BATT_DATA)) {
mSdData.batteryPc = characteristic.getValue()[0];
Log.v(TAG,"Received Battery Data");
mSdData.haveSettings = true;
}
else {
Log.v(TAG,"Unrecognised Characteristic Updated "+
characteristic.getUuid().toString());
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
Log.v(TAG,"onCharacteristicRead");
if (status == BluetoothGatt.GATT_SUCCESS) {
onDataReceived(characteristic);
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
Log.v(TAG,"onCharacteristicChanged(): Characteristic "+characteristic.getUuid()+" changed");
onDataReceived(characteristic);
}
};
/**
* Enables or disables notification on a give characteristic.
*
* @param characteristic Characteristic to act on.
* @param enabled If true, enable notification. False otherwise.
*/
public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
boolean enabled) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter not initialized");
return;
}
Log.v(TAG,"setCharacteristicNotification - Requesting notifications");
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
// Tell the device we want notifications? The sample from Google said we only need this for Heart Rate, but the
// BangleJS widget did not work without it so do it for everything.
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
}
/**
* Retrieves a list of supported GATT services on the connected device. This should be
* invoked only after {@code BluetoothGatt#discoverServices()} completes successfully.
*
* @return A {@code List} of supported services.
*/
public List<BluetoothGattService> getSupportedGattServices() {
if (mBluetoothGatt == null) return null;
return mBluetoothGatt.getServices();
}
}

View File

@@ -53,52 +53,8 @@ import static java.lang.Long.parseLong;
* SdWebServer expects POST requests to /data and /settings URLs to send data or watch settings.
*/
public class SdDataSourceGarmin extends SdDataSource {
private Handler mHandler = new Handler();
private Timer mStatusTimer;
private Timer mSettingsTimer;
private Timer mFaultCheckTimer;
private Time mDataStatusTime;
private boolean mWatchAppRunningCheck = false;
private int mAppRestartTimeout = 10; // Timeout before re-starting watch app (sec) if we have not received
// data after mDataUpdatePeriod
private int mFaultTimerPeriod = 30; // Fault Timer Period in sec
private int mSettingsPeriod = 60; // period between requesting settings in seconds.
private SdDataBroadcastReceiver mSdDataBroadcastReceiver;
private String TAG = "SdDataSourceGarmin";
// Values for SD_MODE
private int SD_MODE_FFT = 0; // The original OpenSeizureDetector mode (FFT based)
private int SD_MODE_RAW = 1; // Send raw, unprocessed data to the phone.
private int SD_MODE_FILTER = 2; // Use digital filter rather than FFT.
private int SIMPLE_SPEC_FMAX = 10;
private int ACCEL_SCALE_FACTOR = 1000; // Amount by which to reduce analysis results to scale to be comparable to analysis on Pebble.
private short mDebug;
private short mFreqCutoff = 12;
private short mDisplaySpectrum;
private short mDataUpdatePeriod;
private short mMutePeriod;
private short mManAlarmPeriod;
private short mPebbleSdMode;
private short mSampleFreq;
private short mAlarmFreqMin;
private short mAlarmFreqMax;
private short mSamplePeriod;
private short mWarnTime;
private short mAlarmTime;
private short mAlarmThresh;
private short mAlarmRatioThresh;
private boolean mFallActive;
private short mFallThreshMin;
private short mFallThreshMax;
private short mFallWindow;
private int mMute; // !=0 means muted by keypress on watch.
private int mAlarmCount;
public SdDataSourceGarmin(Context context, Handler handler,
SdDataReceiver sdDataReceiver) {
super(context, handler, sdDataReceiver);
@@ -116,60 +72,7 @@ public class SdDataSourceGarmin extends SdDataSource {
public void start() {
Log.i(TAG, "start()");
mUtil.writeToSysLogFile("SdDataSourceGarmin.start()");
updatePrefs();
// Start timer to check status of watch regularly.
mDataStatusTime = new Time(Time.getCurrentTimezone());
// use a timer to check the status of the pebble app on the same frequency
// as we get app data.
if (mStatusTimer == null) {
Log.v(TAG, "start(): starting status timer");
mUtil.writeToSysLogFile("SdDataSourceGarmin.start() - starting status timer");
mStatusTimer = new Timer();
mStatusTimer.schedule(new TimerTask() {
@Override
public void run() {
getStatus();
}
}, 0, mDataUpdatePeriod * 1000);
} else {
Log.v(TAG, "start(): status timer already running.");
mUtil.writeToSysLogFile("SdDataSourceGarmin.start() - status timer already running??");
}
if (mFaultCheckTimer == null) {
Log.v(TAG, "start(): starting alarm check timer");
mUtil.writeToSysLogFile("SdDataSourceGarmin.start() - starting alarm check timer");
mFaultCheckTimer = new Timer();
mFaultCheckTimer.schedule(new TimerTask() {
@Override
public void run() {
faultCheck();
}
}, 0, 1000);
} else {
Log.v(TAG, "start(): alarm check timer already running.");
mUtil.writeToSysLogFile("SdDataSourceGarmin.start() - alarm check timer already running??");
}
if (mSettingsTimer == null) {
Log.v(TAG, "start(): starting settings timer");
mUtil.writeToSysLogFile("SdDataSourceGarmin.start() - starting settings timer");
mSettingsTimer = new Timer();
mSettingsTimer.schedule(new TimerTask() {
@Override
public void run() {
mSdData.haveSettings = false;
}
}, 0, 1000 * mSettingsPeriod); // ask for settings less frequently than we get data
} else {
Log.v(TAG, "start(): settings timer already running.");
mUtil.writeToSysLogFile("SdDataSourceGarmin.start() - settings timer already running??");
}
mSdDataBroadcastReceiver = new SdDataBroadcastReceiver();
//uk.org.openseizuredetector.SdDataReceived
IntentFilter filter = new IntentFilter("uk.org.openseizuredetector.SdDataReceived");
mContext.registerReceiver(mSdDataBroadcastReceiver, filter);
super.start();
}
/**
@@ -178,580 +81,8 @@ public class SdDataSourceGarmin extends SdDataSource {
public void stop() {
Log.i(TAG, "stop()");
mUtil.writeToSysLogFile("SdDataSourceGarmin.stop()");
try {
// Stop the status timer
if (mStatusTimer != null) {
Log.v(TAG, "stop(): cancelling status timer");
mUtil.writeToSysLogFile("SdDataSourceGarmin.stop() - cancelling status timer");
mStatusTimer.cancel();
mStatusTimer.purge();
mStatusTimer = null;
}
// Stop the settings timer
if (mSettingsTimer != null) {
Log.v(TAG, "stop(): cancelling settings timer");
mUtil.writeToSysLogFile("SdDataSourceGarmin.stop() - cancelling settings timer");
mSettingsTimer.cancel();
mSettingsTimer.purge();
mSettingsTimer = null;
}
// Stop the alarm check timer
if (mFaultCheckTimer != null) {
Log.v(TAG, "stop(): cancelling alarm check timer");
mUtil.writeToSysLogFile("SdDataSourceGarmin.stop() - cancelling alarm check timer");
mFaultCheckTimer.cancel();
mFaultCheckTimer.purge();
mFaultCheckTimer = null;
}
} catch (Exception e) {
Log.v(TAG, "Error in stop() - " + e.toString());
mUtil.writeToSysLogFile("SdDataSourceGarmin.stop() - error - "+e.toString());
}
mContext.unregisterReceiver(mSdDataBroadcastReceiver);
super.stop();
}
/**
* updatePrefs() - update basic settings from the SharedPreferences
* - defined in res/xml/SdDataSourceNetworkPassivePrefs.xml
*/
public void updatePrefs() {
Log.v(TAG, "updatePrefs()");
mUtil.writeToSysLogFile("SdDataSourceGarmin.updatePrefs()");
SharedPreferences SP = PreferenceManager
.getDefaultSharedPreferences(mContext);
try {
// Parse the AppRestartTimeout period setting.
try {
String appRestartTimeoutStr = SP.getString("AppRestartTimeout", "10");
mAppRestartTimeout = Integer.parseInt(appRestartTimeoutStr);
Log.v(TAG, "updatePrefs() - mAppRestartTimeout = " + mAppRestartTimeout);
} catch (Exception ex) {
Log.v(TAG, "updatePrefs() - Problem with AppRestartTimeout preference!");
Toast toast = Toast.makeText(mContext, "Problem Parsing AppRestartTimeout Preference", Toast.LENGTH_SHORT);
toast.show();
}
// Parse the FaultTimer period setting.
try {
String faultTimerPeriodStr = SP.getString("FaultTimerPeriod", "30");
mFaultTimerPeriod = Integer.parseInt(faultTimerPeriodStr);
Log.v(TAG, "updatePrefs() - mFaultTimerPeriod = " + mFaultTimerPeriod);
} catch (Exception ex) {
Log.v(TAG, "updatePrefs() - Problem with FaultTimerPeriod preference!");
Toast toast = Toast.makeText(mContext, "Problem Parsing FaultTimerPeriod Preference", Toast.LENGTH_SHORT);
toast.show();
}
// Watch Settings
String prefStr;
prefStr = SP.getString("PebbleDebug", "SET_FROM_XML");
if (prefStr != null) {
mDebug = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() Debug = " + mDebug);
prefStr = SP.getString("PebbleDisplaySpectrum", "SET_FROM_XML");
mDisplaySpectrum = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() DisplaySpectrum = " + mDisplaySpectrum);
prefStr = SP.getString("PebbleUpdatePeriod", "SET_FROM_XML");
mDataUpdatePeriod = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() DataUpdatePeriod = " + mDataUpdatePeriod);
prefStr = SP.getString("MutePeriod", "SET_FROM_XML");
mMutePeriod = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() MutePeriod = " + mMutePeriod);
prefStr = SP.getString("ManAlarmPeriod", "SET_FROM_XML");
mManAlarmPeriod = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() ManAlarmPeriod = " + mManAlarmPeriod);
prefStr = SP.getString("PebbleSdMode", "SET_FROM_XML");
mPebbleSdMode = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() PebbleSdMode = " + mPebbleSdMode);
prefStr = SP.getString("SampleFreq", "SET_FROM_XML");
mSampleFreq = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() SampleFreq = " + mSampleFreq);
prefStr = SP.getString("SamplePeriod", "SET_FROM_XML");
mSamplePeriod = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() AnalysisPeriod = " + mSamplePeriod);
prefStr = SP.getString("AlarmFreqMin", "SET_FROM_XML");
mAlarmFreqMin = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() AlarmFreqMin = " + mAlarmFreqMin);
prefStr = SP.getString("AlarmFreqMax", "SET_FROM_XML");
mAlarmFreqMax = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() AlarmFreqMax = " + mAlarmFreqMax);
prefStr = SP.getString("WarnTime", "SET_FROM_XML");
mWarnTime = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() WarnTime = " + mWarnTime);
prefStr = SP.getString("AlarmTime", "SET_FROM_XML");
mAlarmTime = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() AlarmTime = " + mAlarmTime);
prefStr = SP.getString("AlarmThresh", "SET_FROM_XML");
mAlarmThresh = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() AlarmThresh = " + mAlarmThresh);
prefStr = SP.getString("AlarmRatioThresh", "SET_FROM_XML");
mAlarmRatioThresh = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() AlarmRatioThresh = " + mAlarmRatioThresh);
mFallActive = SP.getBoolean("FallActive", false);
Log.v(TAG, "updatePrefs() FallActive = " + mFallActive);
prefStr = SP.getString("FallThreshMin", "SET_FROM_XML");
mFallThreshMin = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() FallThreshMin = " + mFallThreshMin);
prefStr = SP.getString("FallThreshMax", "SET_FROM_XML");
mFallThreshMax = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() FallThreshMax = " + mFallThreshMax);
prefStr = SP.getString("FallWindow", "SET_FROM_XML");
mFallWindow = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() FallWindow = " + mFallWindow);
mSdData.mHRAlarmActive = SP.getBoolean("HRAlarmActive", false);
Log.v(TAG, "updatePrefs() HRAlarmActive = " + mSdData.mHRAlarmActive);
mSdData.mHRNullAsAlarm = SP.getBoolean("HRNullAsAlarm", false);
Log.v(TAG, "updatePrefs() HRNullAsAlarm = " + mSdData.mHRNullAsAlarm);
prefStr = SP.getString("HRThreshMin", "SET_FROM_XML");
mSdData.mHRThreshMin = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() HRThreshMin = " + mSdData.mHRThreshMin);
prefStr = SP.getString("HRThreshMax", "SET_FROM_XML");
mSdData.mHRThreshMax = (short) Integer.parseInt(prefStr);
Log.v(TAG, "updatePrefs() HRThreshMax = " + mSdData.mHRThreshMax);
} else {
Log.v(TAG, "updatePrefs() - prefStr is null - WHY????");
mUtil.writeToSysLogFile("SdDataSourceGarmin.updatePrefs() - prefStr is null - WHY??");
Toast toast = Toast.makeText(mContext, "Problem Parsing Preferences - Something won't work - Please go back to Settings and correct it!", Toast.LENGTH_SHORT);
toast.show();
}
} catch (Exception ex) {
Log.v(TAG, "updatePrefs() - Problem parsing preferences!");
mUtil.writeToSysLogFile("SdDataSourceGarmin.updatePrefs() - ERROR "+ex.toString());
Toast toast = Toast.makeText(mContext, "Problem Parsing Preferences - Something won't work - Please go back to Settings and correct it!", Toast.LENGTH_SHORT);
toast.show();
}
}
// Force the data stored in this datasource to update in line with the JSON string encoded data provided.
// Used by webServer to update the GarminDatasource.
// Returns a message string that is passed back to the watch.
public String updateFromJSON(String jsonStr) {
String retVal = "undefined";
String watchPartNo;
String watchFwVersion;
String sdVersion;
String sdName;
Log.v(TAG,"updateFromJSON - "+jsonStr);
try {
JSONObject mainObject = new JSONObject(jsonStr);
//JSONObject dataObject = mainObject.getJSONObject("dataObj");
JSONObject dataObject = mainObject;
String dataTypeStr = dataObject.getString("dataType");
Log.v(TAG,"updateFromJSON - dataType="+dataTypeStr);
if (dataTypeStr.equals("raw")) {
Log.v(TAG,"updateFromJSON - processing raw data");
try {
mSdData.mHR = dataObject.getDouble("HR");
} catch (JSONException e) {
// if we get 'null' HR (For example if the heart rate is not working)
mSdData.mHR = -1;
}
try {
mMute = dataObject.getInt("Mute");
} catch (JSONException e) {
// if we get 'null' HR (For example if the heart rate is not working)
mMute = 0;
}
JSONArray accelVals = dataObject.getJSONArray("data");
Log.v(TAG, "Received " + accelVals.length() + " acceleration values");
int i;
for (i = 0; i < accelVals.length(); i++) {
mSdData.rawData[i] = accelVals.getInt(i);
}
mSdData.mNsamp = accelVals.length();
//mNSamp = accelVals.length();
mWatchAppRunningCheck = true;
doAnalysis();
if (mSdData.haveSettings == false) {
retVal = "sendSettings";
} else {
retVal = "OK";
}
} else if (dataTypeStr.equals("settings")){
Log.v(TAG,"updateFromJSON - processing settings");
mSamplePeriod = (short)dataObject.getInt("analysisPeriod");
mSampleFreq = (short)dataObject.getInt("sampleFreq");
mSdData.batteryPc = (short)dataObject.getInt("battery");
Log.v(TAG,"updateFromJSON - mSamplePeriod="+mSamplePeriod+" mSampleFreq="+mSampleFreq);
mUtil.writeToSysLogFile("SdDataSourceGarmin.updateFromJSON - Settings Received");
mUtil.writeToSysLogFile(" * mSamplePeriod="+mSamplePeriod+" mSampleFreq="+mSampleFreq);
mUtil.writeToSysLogFile(" * batteryPc = "+mSdData.batteryPc);
try {
watchPartNo = dataObject.getString("watchPartNo");
watchFwVersion = dataObject.getString("watchFwVersion");
sdVersion = dataObject.getString("sdVersion");
sdName = dataObject.getString("sdName");
mUtil.writeToSysLogFile(" * sdName = "+sdName+" version "+sdVersion);
mUtil.writeToSysLogFile(" * watchPartNo = "+watchPartNo+" fwVersion "+watchFwVersion);
} catch (Exception e) {
Log.e(TAG,"updateFromJSON - Error Parsing V3.2 JSON String - "+e.toString());
mUtil.writeToSysLogFile("updateFromJSON - Error Parsing V3.2 JSON String - "+e.toString());
mUtil.writeToSysLogFile(" This is probably because of an out of date watch app - please upgrade!");
e.printStackTrace();
}
mSdData.haveSettings = true;
mSdData.mSampleFreq = mSampleFreq;
mWatchAppRunningCheck = true;
retVal = "OK";
} else {
Log.e(TAG,"updateFromJSON - unrecognised dataType "+dataTypeStr);
retVal = "ERROR";
}
} catch (Exception e) {
Log.e(TAG,"updateFromJSON - Error Parsing JSON String - "+e.toString());
mUtil.writeToSysLogFile("updateFromJSON - Error Parsing JSON String - "+e.toString());
e.printStackTrace();
retVal = "ERROR";
}
return(retVal);
}
/**
* Calculate the magnitude of entry i in the fft array fft
* @param fft
* @param i
* @return magnitude ( Re*Re + Im*Im )
*/
private double getMagnitude(double[] fft, int i) {
double mag;
mag = (fft[2*i]*fft[2*i] + fft[2*i + 1] * fft[2*i +1]);
return mag;
}
/**
* doAnalysis() - analyse the data if the accelerometer data array mAccData
* and populate the output data structure mSdData
*/
private void doAnalysis() {
// FIXME - Use specified sampleFreq, not this hard coded one
mSampleFreq = 25;
double freqRes = 1.0*mSampleFreq/mSdData.mNsamp;
Log.v(TAG,"doAnalysis(): mSampleFreq="+mSampleFreq+" mNSamp="+mSdData.mNsamp+": freqRes="+freqRes);
// Set the frequency bounds for the analysis in fft output bin numbers.
int nMin = (int)(mAlarmFreqMin/freqRes);
int nMax = (int)(mAlarmFreqMax /freqRes);
Log.v(TAG,"doAnalysis(): mAlarmFreqMin="+mAlarmFreqMin+", nMin="+nMin
+", mAlarmFreqMax="+mAlarmFreqMax+", nMax="+nMax);
// Calculate the bin number of the cutoff frequency
int nFreqCutoff = (int)(mFreqCutoff /freqRes);
Log.v(TAG,"mFreqCutoff = "+mFreqCutoff+", nFreqCutoff="+nFreqCutoff);
DoubleFFT_1D fftDo = new DoubleFFT_1D(mSdData.mNsamp);
double[] fft = new double[mSdData.mNsamp * 2];
///System.arraycopy(mAccData, 0, fft, 0, mNsamp);
System.arraycopy(mSdData.rawData, 0, fft, 0, mSdData.mNsamp);
fftDo.realForward(fft);
// Calculate the whole spectrum power (well a value equivalent to it that avoids square root calculations
// and zero any readings that are above the frequency cutoff.
double specPower = 0;
for (int i = 1; i < mSdData.mNsamp / 2; i++) {
if (i <= nFreqCutoff) {
specPower = specPower + getMagnitude(fft,i);
} else {
fft[2*i] = 0.;
fft[2*i+1] = 0.;
}
}
//Log.v(TAG,"specPower = "+specPower);
//specPower = specPower/(mSdData.mNsamp/2);
specPower = specPower/mSdData.mNsamp/2;
//Log.v(TAG,"specPower = "+specPower);
// Calculate the Region of Interest power and power ratio.
double roiPower = 0;
for (int i=nMin;i<nMax;i++) {
roiPower = roiPower + getMagnitude(fft,i);
}
roiPower = roiPower/(nMax - nMin);
double roiRatio = 10 * roiPower / specPower;
// Calculate the simplified spectrum - power in 1Hz bins.
double[] simpleSpec = new double[SIMPLE_SPEC_FMAX+1];
for (int ifreq=0;ifreq<SIMPLE_SPEC_FMAX;ifreq++) {
int binMin = (int)(1 + ifreq/freqRes); // add 1 to loose dc component
int binMax = (int)(1 + (ifreq+1)/freqRes);
simpleSpec[ifreq]=0;
for (int i=binMin;i<binMax;i++) {
simpleSpec[ifreq] = simpleSpec[ifreq] + getMagnitude(fft,i);
}
simpleSpec[ifreq] = simpleSpec[ifreq] / (binMax-binMin);
}
// Populate the mSdData structure to communicate with the main SdServer service.
mDataStatusTime.setToNow();
mSdData.specPower = (long)specPower / ACCEL_SCALE_FACTOR;
mSdData.roiPower = (long)roiPower / ACCEL_SCALE_FACTOR;
mSdData.dataTime.setToNow();
mSdData.maxVal = 0; // not used
mSdData.maxFreq = 0; // not used
mSdData.haveData = true;
mSdData.alarmThresh = mAlarmThresh;
mSdData.alarmRatioThresh = mAlarmRatioThresh;
mSdData.alarmFreqMin = mAlarmFreqMin;
mSdData.alarmFreqMax = mAlarmFreqMax;
// note mSdData.batteryPc is set from settings data in updateFromJSON()
// FIXME - I haven't worked out why dividing by 1000 seems necessary to get the graph on scale - we don't seem to do that with the Pebble.
for(int i=0;i<SIMPLE_SPEC_FMAX;i++) {
mSdData.simpleSpec[i] = (int)simpleSpec[i]/ACCEL_SCALE_FACTOR;
}
Log.v(TAG, "simpleSpec = " + Arrays.toString(mSdData.simpleSpec));
// Because we have received data, set flag to show watch app running.
mWatchAppRunningCheck = true;
// Check this data to see if it represents an alarm state.
alarmCheck();
hrCheck();
fallCheck();
muteCheck();
mSdDataReceiver.onSdDataReceived(mSdData); // and tell SdServer we have received data.
}
/****************************************************************
* checkAlarm() - checks the current accelerometer data and uses
* historical data to determine if we are in a fault, warning or ok
* state.
* Sets mSdData.alarmState and mSdData.hrAlarmStanding
*/
private void alarmCheck() {
boolean inAlarm;
Log.v(TAG, "alarmCheck()");
// Is the current set of data representing an alarm state?
if ((mSdData.roiPower > mAlarmThresh) && (10 * (mSdData.roiPower / mSdData.specPower) > mAlarmRatioThresh)) {
inAlarm = true;
} else {
inAlarm = false;
}
// set the alarmState to Alarm, Warning or OK, depending on the current state and previous ones.
if (inAlarm) {
mAlarmCount += mSamplePeriod;
if (mAlarmCount > mAlarmTime) {
// full alarm
mSdData.alarmState = 2;
} else if (mAlarmCount > mWarnTime) {
// warning
mSdData.alarmState = 1;
}
} else {
// If we are not in an ALARM state, revert back to WARNING, otherwise
// revert back to OK.
if (mSdData.alarmState == 2) {
// revert to warning
mSdData.alarmState = 1;
mAlarmCount = mWarnTime + 1; // pretend we have only just entered warning state.
} else {
// revert to OK
mSdData.alarmState = 0;
mAlarmCount = 0;
}
}
Log.v(TAG, "alarmCheck(): inAlarm=" + inAlarm + ", alarmState = " + mSdData.alarmState + " alarmCount=" + mAlarmCount + " mAlarmTime=" + mAlarmTime);
}
public void muteCheck() {
if (mMute != 0) {
Log.v(TAG, "Mute Active - setting alarms to mute");
mSdData.alarmState = 6;
mSdData.alarmPhrase = "MUTE";
mSdData.mHRAlarmStanding = false;
}
}
/**
* hrCheck - check the Heart rate data in mSdData to see if it represents an alarm condition.
* Sets mSdData.mHRAlarmStanding
*/
public void hrCheck() {
Log.v(TAG, "hrCheck()");
/* Check Heart Rate against alarm settings */
if (mSdData.mHRAlarmActive) {
if (mSdData.mHR < 0) {
if (mSdData.mHRNullAsAlarm) {
Log.i(TAG, "Heart Rate Null - Alarming");
mSdData.mHRFaultStanding = false;
mSdData.mHRAlarmStanding = true;
} else {
Log.i(TAG, "Heart Rate Fault (HR<0)");
mSdData.mHRFaultStanding = true;
mSdData.mHRAlarmStanding = false;
}
}
else if ((mSdData.mHR > mSdData.mHRThreshMax) || (mSdData.mHR < mSdData.mHRThreshMin)) {
Log.i(TAG, "Heart Rate Abnormal - " + mSdData.mHR + " bpm");
mSdData.mHRFaultStanding = false;
mSdData.mHRAlarmStanding = true;
}
else {
mSdData.mHRFaultStanding = false;
mSdData.mHRAlarmStanding = false;
}
}
}
/****************************************************************
* Simple threshold analysis to chech for fall.
* Called from clock_tick_handler()
*/
public void fallCheck() {
int i,j;
double minAcc, maxAcc;
long fallWindowSamp = (mFallWindow*mSdData.mSampleFreq)/1000; // Convert ms to samples.
Log.v(TAG, "check_fall() - fallWindowSamp=" +fallWindowSamp);
// Move window through sample buffer, checking for fall.
// Note - not resetting fallAlarmStanding means that fall alarms will always latch until the 'Accept Alarm' button
// is pressed.
//mSdData.fallAlarmStanding = false;
if (mFallActive) {
mSdData.mFallActive = true;
for (i = 0; i < mSdData.mNsamp - fallWindowSamp; i++) { // i = window start point
// Find max and min acceleration within window.
minAcc = mSdData.rawData[i];
maxAcc = mSdData.rawData[i];
for (j = 0; j < fallWindowSamp; j++) { // j = position within window
if (mSdData.rawData[i + j] < minAcc) minAcc = mSdData.rawData[i + j];
if (mSdData.rawData[i + j] > maxAcc) maxAcc = mSdData.rawData[i + j];
}
if ((minAcc < mFallThreshMin) && (maxAcc > mFallThreshMax)) {
Log.d(TAG, "check_fall() - minAcc=" + minAcc + ", maxAcc=" + maxAcc);
Log.d(TAG, "check_fall() - ****FALL DETECTED****");
mSdData.fallAlarmStanding = true;
return;
}
if (mMute != 0) {
Log.v(TAG,"Mute Active - setting fall alarm to mute");
mSdData.fallAlarmStanding = false;
}
}
} else {
mSdData.mFallActive = false;
Log.v(TAG,"check_fall - mFallActive is false - doing nothing");
}
//if (debug) APP_LOG(APP_LOG_LEVEL_DEBUG,"check_fall() - minAcc=%d, maxAcc=%d",
// minAcc,maxAcc);
}
/**
* Checks the status of the connection to the watch,
* and sets class variables for use by other functions.
*/
public void getStatus() {
Time tnow = new Time(Time.getCurrentTimezone());
long tdiff;
tnow.setToNow();
// get time since the last data was received from the Pebble watch.
tdiff = (tnow.toMillis(false) - mDataStatusTime.toMillis(false));
Log.v(TAG, "getStatus() - mWatchAppRunningCheck=" + mWatchAppRunningCheck + " tdiff=" + tdiff);
Log.v(TAG,"getStatus() - tdiff="+tdiff+", mDataUpatePeriod="+mDataUpdatePeriod+", mAppRestartTimeout="+mAppRestartTimeout);
mSdData.watchConnected = true; // We can't check connection for passive network connection, so set it to true to avoid errors.
// And is the watch app running?
// set mWatchAppRunningCheck has been false for more than 10 seconds
// the app is not talking to us
// mWatchAppRunningCheck is set to true in the receiveData handler.
if (!mWatchAppRunningCheck &&
(tdiff > (mDataUpdatePeriod + mAppRestartTimeout) * 1000)) {
Log.v(TAG, "getStatus() - tdiff = " + tdiff);
mSdData.watchAppRunning = false;
// Only make audible warning beep if we have not received data for more than mFaultTimerPeriod seconds.
if (tdiff > (mDataUpdatePeriod + mFaultTimerPeriod) * 1000) {
Log.v(TAG, "getStatus() - Watch App Not Running");
mUtil.writeToSysLogFile("SdDataSourceGarmin.getStatus() - Watch App not Running");
//mDataStatusTime.setToNow();
mSdData.roiPower = -1;
mSdData.specPower = -1;
mSdDataReceiver.onSdDataFault(mSdData);
} else {
Log.v(TAG, "getStatus() - Waiting for mFaultTimerPeriod before issuing audible warning...");
}
} else {
mSdData.watchAppRunning = true;
}
// if we have confirmation that the app is running, reset the
// status time to now and initiate another check.
if (mWatchAppRunningCheck) {
mWatchAppRunningCheck = false;
mDataStatusTime.setToNow();
}
if (!mSdData.haveSettings) {
Log.v(TAG, "getStatus() - no settings received yet");
}
}
/**
* faultCheck - determines alarm state based on seizure detector data SdData. Called every second.
*/
private void faultCheck() {
Time tnow = new Time(Time.getCurrentTimezone());
long tdiff;
tnow.setToNow();
// get time since the last data was received from the watch.
tdiff = (tnow.toMillis(false) - mDataStatusTime.toMillis(false));
Log.v(TAG, "faultCheck() - tdiff=" + tdiff + ", mDataUpatePeriod=" + mDataUpdatePeriod + ", mAppRestartTimeout=" + mAppRestartTimeout
+ ", combined = " + (mDataUpdatePeriod + mAppRestartTimeout) * 1000);
if (!mWatchAppRunningCheck &&
(tdiff > (mDataUpdatePeriod + mAppRestartTimeout) * 1000)) {
Log.v(TAG, "faultCheck() - watch app not running so not doing anything");
mAlarmCount = 0;
}
}
public class SdDataBroadcastReceiver extends BroadcastReceiver {
//private String TAG = "SdDataBroadcastReceiver";
@Override
public void onReceive(Context context, Intent intent) {
Log.v(TAG,"SdDataBroadcastReceiver.onReceive()");
String jsonStr = intent.getStringExtra("data");
Log.v(TAG,"SdDataBroadcastReceiver.onReceive() - data="+jsonStr);
updateFromJSON(jsonStr);
}
}
}

View File

@@ -244,6 +244,11 @@ public class SdServer extends Service implements SdDataReceiver {
mUtil.writeToSysLogFile("SdServer.onStartCommand() - creating SdDataSourceGarmin");
mSdDataSource = new SdDataSourceGarmin(this.getApplicationContext(), mHandler, this);
break;
case "BLE":
Log.v(TAG, "Selecting BLE DataSource");
mUtil.writeToSysLogFile("SdServer.onStartCommand() - creating SdDataSourceBLE");
mSdDataSource = new SdDataSourceBLE(this.getApplicationContext(), mHandler, this);
break;
default:
Log.e(TAG, "Datasource " + mSdDataSourceName + " not recognised - Defaulting to Pebble");
mUtil.writeToSysLogFile("SdServer.onStartCommand() - Datasource " + mSdDataSourceName + " not recognised - exiting");
@@ -679,10 +684,24 @@ public class SdServer extends Service implements SdDataReceiver {
mSdData.alarmPhrase = "FAULT";
mSdData.alarmStanding = false;
if (webServer != null) webServer.setSdData(mSdData);
if (mAudibleFaultWarning) {
// We only take action to warn the user and re-start the data source to attempt to fix it
// ourselves if we have been in a fault condition for a while - signified by the mFaultTimerCompleted
// flag.
if (mFaultTimerCompleted) {
faultWarningBeep();
//mSdDataSource.stop();
//mHandler.postDelayed(new Runnable() {
// public void run() {
// mSdDataSource.start();
// }
//}, 190);
} else {
startFaultTimer();
Log.v(TAG, "onSdDataFault() - starting Fault Timer");
mUtil.writeToSysLogFile("onSdDataFault() - starting Fault Timer");
}
showNotification(-1);
showNotification(-1);
}
/* from http://stackoverflow.com/questions/12154940/how-to-make-a-beep-in-android */
@@ -705,26 +724,20 @@ public class SdServer extends Service implements SdDataReceiver {
* beep, provided mAudibleAlarm is set
*/
public void faultWarningBeep() {
if (mFaultTimerCompleted) {
if (mCancelAudible) {
if (mCancelAudible) {
Log.v(TAG, "faultWarningBeep() - CancelAudible Active - silent beep...");
} else {
if (mAudibleFaultWarning) {
if (mMp3Alarm) {
Log.v(TAG, "Not making MP3 fault beep - handled by notification");
} else {
beep(10);
}
Log.v(TAG, "faultWarningBeep()");
mUtil.writeToSysLogFile("SdServer.faultWarningBeep() - beeping");
if (mAudibleFaultWarning) {
if (mMp3Alarm) {
Log.v(TAG, "Not making MP3 fault beep - handled by notification");
} else {
Log.v(TAG, "faultWarningBeep() - silent...");
beep(10);
}
Log.v(TAG, "faultWarningBeep()");
mUtil.writeToSysLogFile("SdServer.faultWarningBeep() - beeping");
} else {
Log.v(TAG, "faultWarningBeep() - silent...");
}
} else {
startFaultTimer();
Log.v(TAG, "faultWarningBeep() - starting Fault Timer");
mUtil.writeToSysLogFile("faultWarningBeep() - starting Fault Timer");
}
}