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

@@ -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

View File

@@ -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"
}
]
}

View File

@@ -5,6 +5,8 @@
android:versionCode="72"
android:versionName="3.5.0">
<!-- android:allowBackup="false" -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -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"
> <!--@android:style/Theme.Holo.Light"-->
<activity android:name=".BLEScanActivity"></activity>
<activity android:name=".DBQueryActivity"></activity>
<!-- android:usesCleartextTraffic="true" -->
<activity android:name=".StartupActivity">
@@ -69,4 +72,4 @@
android:required="false" />
</application>
</manifest>
</manifest>

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");
}
}

View File

@@ -0,0 +1,23 @@
<!--
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="56dp"
android:minWidth="56dp">
<ProgressBar android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"/>
</FrameLayout>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView android:id="@+id/device_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24dp"/>
<TextView android:id="@+id/device_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12dp"/>
</LinearLayout>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<Button
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/selectBLEDeviceButton"
android:text="@string/select_ble_device_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onClick"
>
</Button>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/menu_refresh"
android:checkable="false"
android:orderInCategory="1"
app:showAsAction="ifRoom" />
<item android:id="@+id/menu_scan"
android:title="@string/menu_scan"
android:orderInCategory="100"
app:showAsAction="ifRoom|withText" />
<item android:id="@+id/menu_stop"
android:title="@string/menu_stop"
android:orderInCategory="101"
app:showAsAction="ifRoom|withText" />
</menu>

View File

@@ -4,11 +4,13 @@
<item>"Pebble Watch"</item>
<item>"Garmin Watch"</item>
<item>"Network"</item>
<item>"Bluetooth Device"</item>
</string-array>
<string-array name="datasource_list_values">
<item>"Pebble"</item>
<item>"Garmin"</item>
<item>"Network"</item>
<item>"BLE"</item>
</string-array>
</resources>

View File

@@ -4,6 +4,8 @@
<string name="changelog">
\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
</string>
<string name="UpgradeMsg">
OpenSeizureDetector does not collect any personal data.
@@ -135,5 +137,12 @@
<string name="phone_alarm_disabled">Phone Alarm Disabled</string>
<string name="test_phone_alarm_notification">Test Phone Alarm</string>
<string name="DiallerNotInstalledWarning"><a href="https://github.com/OpenSeizureDetector/Dialler/tree/master/app/release/app-release.apk">OpenSeizureDetector Dialer App</a> Not installed - Required for Phone Call Alerts.</string>
<string name="title_devices">BLE Devices</string>
<string name="ble_not_supported">BLE Not Supported</string>
<string name="error_bluetooth_not_supported">Bluetooth Not Supported</string>
<string name="menu_stop">STOP</string>
<string name="menu_scan">SCAN</string>
<string name="unknown_device">Unknown Device</string>
<string name="select_ble_device_title">Select BLE Device</string>
<string name="select_ble_device_desc">Select Bluetooth Low Energy (BLE) Device to provide seizure (acceleration and heart rate) data).</string>
</resources>

View File

@@ -8,6 +8,12 @@
android:entryValues="@array/datasource_list_values"
android:defaultValue="Garmin"
android:dialogTitle="Select Data Source" />
<Preference
android:key="SelectBLEDevice"
android:title="@string/select_ble_device_title"
android:summary="@string/select_ble_device_desc"
android:widgetLayout="@layout/pref_select_ble_device_button"
/>
<CheckBoxPreference
android:defaultValue="true"
android:key="LogAlarms"

View File

@@ -1,7 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The ListPreference data is defined in pebble_datasource_values.xml -->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<?xml version="1.0" encoding="utf-8"?><!-- The ListPreference data is defined in pebble_datasource_values.xml -->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="BLE Device Settings">
<EditTextPreference
android:defaultValue=""
android:key="BLE_Device_Addr"
android:summary="MAC Address of BLE Data Source Device"
android:title="Device Address" />
<EditTextPreference
android:defaultValue=""
android:key="BLE_Device_Name"
android:summary="Name of BLE Data Source Device"
android:title="Device Address" />
</PreferenceCategory>
<PreferenceCategory android:title="User Interface Settings">
<EditTextPreference
android:defaultValue="5"
@@ -58,23 +68,22 @@
android:summary="Period (in seconds) between data analyses"
android:title="SamplePeriod (sec)" />
<ListPreference
android:key="PebbleSdMode"
android:title="Seizure Detector Mode"
android:summary="Select one of the three available modes of operation."
android:defaultValue="0"
android:dialogTitle="Select Seizure Detector Mode"
android:entries="@array/pebble_sd_mode_list"
android:entryValues="@array/pebble_sd_mode_list_values"
android:defaultValue="0"
android:dialogTitle="Select Seizure Detector Mode" />
android:key="PebbleSdMode"
android:summary="Select one of the three available modes of operation."
android:title="Seizure Detector Mode" />
<ListPreference
android:key="SampleFreq"
android:title="Select Sample Frequency"
android:summary="Higher Frequency is more Accurate, but uses more battery power."
android:entries="@array/pebble_sample_freq_list"
android:entryValues="@array/pebble_sample_freq_list_values"
android:defaultValue="100"
android:dialogTitle="Select Sample Frequency"
android:enabled="true"
/>
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" />
</PreferenceCategory>
@@ -102,16 +111,15 @@
android:title="Fall Detection Window (milli-seconds)" />
</PreferenceCategory>
<PreferenceCategory
android:title="Watch Communications Settings">
<PreferenceCategory android:title="Watch Communications Settings">
<ListPreference
android:key="PebbleDebug"
android:title="Seizure Detector Debug Mode"
android:summary="Set Debug mode on or off."
android:defaultValue="0"
android:dialogTitle="Select Debug Mode"
android:entries="@array/pebble_debug_list"
android:entryValues="@array/pebble_debug_values"
android:defaultValue="0"
android:dialogTitle="Select Debug Mode" />
android:key="PebbleDebug"
android:summary="Set Debug mode on or off."
android:title="Seizure Detector Debug Mode" />
<EditTextPreference
android:defaultValue="10"
android:key="AppRestartTimeout"