diff --git a/CHANGELOG.md b/CHANGELOG.md index 254780f..b22c02d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ V3.5.0 - Aug 2020 - Added broadcast to request phone call dial alert (handled by separate app OpenSeizureDetector Dialler). - Added UUID string to SMS alerts so they can be detected by a custom SMS receiver on the carer's phone. - + V3.4.0 - Aug2020 + - Added support for BLE data source V3.2.1 - Aug2020 - Addition of Spanish Translation, and correction of crash report wording in German. V3.2.0 - mar2020 diff --git a/app/release/app-release.apk b/app/release/app-release-3.4.0a.apk similarity index 100% rename from app/release/app-release.apk rename to app/release/app-release-3.4.0a.apk diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..1fb9a09 --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "uk.org.openseizuredetector", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "properties": [], + "versionCode": 72, + "versionName": "3.4.0", + "enabled": true, + "outputFile": "app-release.apk" + } + ] +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8d623cb..8af6043 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,8 @@ android:versionCode="72" android:versionName="3.5.0"> + + @@ -27,8 +29,9 @@ android:icon="@drawable/star_of_life_48x48" android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" - android:theme="@style/Theme.AppCompat" - > + android:theme="@style/AppTheme" + > + @@ -69,4 +72,4 @@ android:required="false" /> - \ No newline at end of file + diff --git a/app/src/main/java/uk/org/openseizuredetector/BLEScanActivity.java b/app/src/main/java/uk/org/openseizuredetector/BLEScanActivity.java new file mode 100644 index 0000000..7d2daf1 --- /dev/null +++ b/app/src/main/java/uk/org/openseizuredetector/BLEScanActivity.java @@ -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 mLeDevices; + private LayoutInflater mInflator; + + public LeDeviceListAdapter() { + super(); + mLeDevices = new ArrayList(); + 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; + } + } +} diff --git a/app/src/main/java/uk/org/openseizuredetector/GattAttributes.java b/app/src/main/java/uk/org/openseizuredetector/GattAttributes.java new file mode 100644 index 0000000..9d890f1 --- /dev/null +++ b/app/src/main/java/uk/org/openseizuredetector/GattAttributes.java @@ -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 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; + } +} diff --git a/app/src/main/java/uk/org/openseizuredetector/PrefActivity.java b/app/src/main/java/uk/org/openseizuredetector/PrefActivity.java index 6f402bb..62f31b2 100644 --- a/app/src/main/java/uk/org/openseizuredetector/PrefActivity.java +++ b/app/src/main/java/uk/org/openseizuredetector/PrefActivity.java @@ -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. */ diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java index 316a721..e8501e8 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSource.java @@ -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 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); + } + } + + } diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceBLE.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceBLE.java new file mode 100644 index 0000000..9ae0f57 --- /dev/null +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceBLE.java @@ -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 . + +*/ +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 serviceList = mBluetoothGatt.getServices(); + for (int i = 0; i < serviceList.size(); i++) { + String uuidStr = serviceList.get(i).getUuid().toString(); + Log.v(TAG, "Service " + uuidStr); + List 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 getSupportedGattServices() { + if (mBluetoothGatt == null) return null; + + return mBluetoothGatt.getServices(); + } + + +} + + + + + + + + + diff --git a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceGarmin.java b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceGarmin.java index 0199691..7dbf47d 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdDataSourceGarmin.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdDataSourceGarmin.java @@ -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 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); - } - } - } diff --git a/app/src/main/java/uk/org/openseizuredetector/SdServer.java b/app/src/main/java/uk/org/openseizuredetector/SdServer.java index c5ddbde..b93fb72 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SdServer.java +++ b/app/src/main/java/uk/org/openseizuredetector/SdServer.java @@ -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"); } } diff --git a/app/src/main/res/layout/actionbar_indeterminate_progress.xml b/app/src/main/res/layout/actionbar_indeterminate_progress.xml new file mode 100644 index 0000000..edaac11 --- /dev/null +++ b/app/src/main/res/layout/actionbar_indeterminate_progress.xml @@ -0,0 +1,23 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/ble_list_item_device.xml b/app/src/main/res/layout/ble_list_item_device.xml new file mode 100644 index 0000000..255e412 --- /dev/null +++ b/app/src/main/res/layout/ble_list_item_device.xml @@ -0,0 +1,28 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pref_select_ble_device_button.xml b/app/src/main/res/layout/pref_select_ble_device_button.xml new file mode 100644 index 0000000..d6718b1 --- /dev/null +++ b/app/src/main/res/layout/pref_select_ble_device_button.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/src/main/res/menu/ble_scan_menu.xml b/app/src/main/res/menu/ble_scan_menu.xml new file mode 100644 index 0000000..8b934f1 --- /dev/null +++ b/app/src/main/res/menu/ble_scan_menu.xml @@ -0,0 +1,30 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/datasource_list.xml b/app/src/main/res/values/datasource_list.xml index 06374d4..c8a5d21 100644 --- a/app/src/main/res/values/datasource_list.xml +++ b/app/src/main/res/values/datasource_list.xml @@ -4,11 +4,13 @@ "Pebble Watch" "Garmin Watch" "Network" + "Bluetooth Device" "Pebble" "Garmin" "Network" + "BLE" \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 410e066..8d13d90 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,6 +4,8 @@ \n V3.5 - Added support for Phone Call Alerts, using separate OpenSeizureDetector Dialler App \n - Added Unique Identifier (UUID) to SMS alerts so they can be detected as OpenSeizureDetector SMS messages on client phone. + \n V3.4.0 - Aug 2020 + \n - Added support for BLE Data Source OpenSeizureDetector does not collect any personal data. @@ -135,5 +137,12 @@ Phone Alarm Disabled Test Phone Alarm OpenSeizureDetector Dialer App Not installed - Required for Phone Call Alerts. - + BLE Devices + BLE Not Supported + Bluetooth Not Supported + STOP + SCAN + Unknown Device + Select BLE Device + Select Bluetooth Low Energy (BLE) Device to provide seizure (acceleration and heart rate) data). diff --git a/app/src/main/res/xml/general_prefs.xml b/app/src/main/res/xml/general_prefs.xml index 8577261..9e1408a 100644 --- a/app/src/main/res/xml/general_prefs.xml +++ b/app/src/main/res/xml/general_prefs.xml @@ -8,6 +8,12 @@ android:entryValues="@array/datasource_list_values" android:defaultValue="Garmin" android:dialogTitle="Select Data Source" /> + - - + + + + + + + android:key="PebbleSdMode" + android:summary="Select one of the three available modes of operation." + android:title="Seizure Detector Mode" /> + android:entries="@array/pebble_sample_freq_list" + android:entryValues="@array/pebble_sample_freq_list_values" + android:key="SampleFreq" + android:summary="Higher Frequency is more Accurate, but uses more battery power." + android:title="Select Sample Frequency" /> @@ -102,16 +111,15 @@ android:title="Fall Detection Window (milli-seconds)" /> - + + android:key="PebbleDebug" + android:summary="Set Debug mode on or off." + android:title="Seizure Detector Debug Mode" />