Merge pull request #173 from OpenSeizureDetector/169-v424+-ble-data-source-issues_2nd_attempt

169 v424+ ble data source issues 2nd attempt
This commit is contained in:
Graham Jones
2024-04-13 22:39:08 +01:00
committed by GitHub
16 changed files with 416 additions and 33 deletions

View File

@@ -4,6 +4,7 @@
- Improved start-up checks for permissions
- Improved system re-start after changing settings (but still not perfect!)
- Disabled the CNN algorithm by default as it is causing some false alarms (Issue #170)
- Added watch signal strength history graph and watch battery hisory graph to main activity
V4.2.5 - Set BLE device time if the characteristic is available.
V4.2.4 - Added checks and a FAULT condition for Bluetooth errors in Bluetooth Data Source
V4.2.3 - Uses 3d accelerometer data to calculate magnitude if vector magnitude is not sent from data source.

View File

@@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:versionCode="139"
android:versionName="4.2.6c">
android:versionName="4.2.6">
<!-- android:allowBackup="false" -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

View File

@@ -64,7 +64,7 @@ public class FragmentBatt extends FragmentOsdBaseClass {
@Override
public void onResume() {
super.onResume();
mLineChart = mRootView.findViewById(R.id.lineChart);
mLineChart = mRootView.findViewById(R.id.battLineChart);
mLineChart.getLegend().setEnabled(false);
XAxis xAxis = mLineChart.getXAxis();
xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
@@ -76,8 +76,8 @@ public class FragmentBatt extends FragmentOsdBaseClass {
xAxis.setTextColor(Color.WHITE);
YAxis yAxis = mLineChart.getAxisLeft();
yAxis.setAxisMinValue(40f);
yAxis.setAxisMaxValue(240f);
yAxis.setAxisMinValue(0f);
yAxis.setAxisMaxValue(100f);
yAxis.setDrawGridLines(true);
yAxis.setDrawLabels(true);
yAxis.setTextColor(Color.WHITE);
@@ -111,6 +111,7 @@ public class FragmentBatt extends FragmentOsdBaseClass {
double watchBattArr[] = mConnection.mSdServer.mSdData.watchBattBuff.getVals(); // This gives us a simple vector of hr values to plot.
int nPhoneBattArr = mConnection.mSdServer.mSdData.phoneBattBuff.getNumVals();
double phoneBattArr[] = mConnection.mSdServer.mSdData.phoneBattBuff.getVals();
Log.i(TAG,"updateUi() - nWatchBattArr="+nWatchBattArr+", nPhoneBattArr="+nPhoneBattArr);
if (Objects.nonNull(mConnection.mSdServer.mSdData.watchBattBuff) && nWatchBattArr > 0) {
Log.v(TAG, "hrWatchBattBuff.getNumVals=" + nWatchBattArr);
lineDataSet.clear();

View File

@@ -193,10 +193,11 @@ public class FragmentCommon extends FragmentOsdBaseClass {
tv.setText(getString(R.string.DataSource) + " = " + "Phone (Demo Mode)");
tv.setBackgroundColor(warnColour);
tv.setTextColor(warnTextColour);
} else if (mConnection.mSdServer.mSdDataSourceName.equals("BLE")) {
} else if (mConnection.mSdServer.mSdDataSourceName.equals("BLE")
|| mConnection.mSdServer.mSdDataSourceName.equals("BLE2")) {
tv.setText(getString(R.string.DataSource) + " = " + mConnection.mSdServer.mSdDataSourceName
+ " ("+ mConnection.mSdServer.mSdData.watchSdName + ", "
+ mConnection.mSdServer.mSdData.watchPartNo+")");
+ mConnection.mSdServer.mSdData.watchSerNo+")");
} else {
tv.setText(getString(R.string.DataSource) + " = " + mConnection.mSdServer.mSdDataSourceName);
}

View File

@@ -174,6 +174,20 @@ public class FragmentSystem extends FragmentOsdBaseClass {
tv.setBackgroundColor(okColour);
tv.setTextColor(okTextColour);
}
tv = (TextView) mRootView.findViewById(R.id.watch_manuf_tv);
tv.setText(mConnection.mSdServer.mSdData.watchManuf);
tv = (TextView) mRootView.findViewById(R.id.watch_partno_tv);
tv.setText(mConnection.mSdServer.mSdData.watchPartNo);
tv = (TextView) mRootView.findViewById(R.id.watch_fwver_tv);
tv.setText(mConnection.mSdServer.mSdData.watchFwVersion);
tv = (TextView) mRootView.findViewById(R.id.watch_sdname_tv);
tv.setText(mConnection.mSdServer.mSdData.watchSdName);
tv = (TextView) mRootView.findViewById(R.id.watch_sdver_tv);
tv.setText(mConnection.mSdServer.mSdData.watchSdVersion);
tv = (TextView) mRootView.findViewById(R.id.watch_batt_tv);
tv.setText(mConnection.mSdServer.mSdData.batteryPc+" %");
tv = (TextView) mRootView.findViewById(R.id.watch_signal_tv);
tv.setText(String.format("%.0f dB", mConnection.mSdServer.mSdData.watchSignalStrength));
}
} catch (Exception e) {
Log.e(TAG, "UpdateUi: Exception - ");

View File

@@ -0,0 +1,148 @@
package uk.org.openseizuredetector;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.appcompat.widget.SwitchCompat;
import com.github.mikephil.charting.charts.LineChart;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import com.github.mikephil.charting.utils.ValueFormatter;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
public class FragmentWatchSig extends FragmentOsdBaseClass {
String TAG = "FragmentWatchSig";
LineChart mLineChart;
LineData lineData;
LineDataSet lineDataSet;
List<Entry> sigHistory = new ArrayList<>();
List<String> hrHistoryStrings = new ArrayList<>();
private TextView tvCurrSigStren;
public FragmentWatchSig() {
// Required empty public constructor
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
lineDataSet = new LineDataSet(new ArrayList<Entry>(), "Watch Signal Strength history");
//lineDataSet.setColors(ColorTemplate.JOYFUL_COLORS);
lineDataSet.setValueTextColor(Color.BLACK);
lineDataSet.setValueTextSize(18f);
lineDataSet.setDrawValues(false);
lineDataSet.setCircleSize(0f);
lineDataSet.setLineWidth(3f);
//lineDataSetAverage = new LineDataSet(new ArrayList<Entry>(),"Heart rate history" );
//lineDataSetAverage.setColors(ColorTemplate.JOYFUL_COLORS);
//lineDataSetAverage.setValueTextColor(Color.BLACK);
//lineDataSetAverage.setValueTextSize(18f);
}
@Override
public void onResume() {
super.onResume();
mLineChart = mRootView.findViewById(R.id.sigStrengthLineChart);
mLineChart.getLegend().setEnabled(false);
XAxis xAxis = mLineChart.getXAxis();
xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
xAxis.setTextSize(10f);
xAxis.setDrawAxisLine(true);
xAxis.setDrawLabels(true);
// Note: the default text colour is BLACK, so does not show up on black background!!!
// This took a lot of finding....
xAxis.setTextColor(Color.WHITE);
YAxis yAxis = mLineChart.getAxisLeft();
//yAxis.setAxisMinValue(40f);
//yAxis.setAxisMaxValue(240f);
yAxis.setDrawGridLines(true);
yAxis.setDrawLabels(true);
yAxis.setTextColor(Color.WHITE);
// Inhibit the decimal part of the y axis labels.
yAxis.setValueFormatter(new ValueFormatter() {
@Override
public String getFormattedValue(float v) {
DecimalFormat format = new DecimalFormat("###");
return format.format(v);
}
});
YAxis yAxis2 = mLineChart.getAxisRight();
yAxis2.setDrawGridLines(false);
yAxis2.setEnabled(false);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_watch_sig, container, false);
}
@Override
protected void updateUi() {
Log.d(TAG, "updateUi()");
tvCurrSigStren = (TextView) mRootView.findViewById(R.id.current_sig_strength_tv);
if (mConnection.mBound) {
if (Objects.nonNull(tvCurrSigStren))
tvCurrSigStren.setText(String.valueOf((int) mConnection.mSdServer.mSdData.watchSignalStrength));
double histArr[] = mConnection.mSdServer.mSdData.watchSignalStrengthBuff.getVals();
int nHist = histArr.length;
if (Objects.nonNull(histArr) && nHist > 0) {
Log.v(TAG, "nHist=" + nHist);
lineDataSet.clear();
String xVals[] = new String[nHist];
for (int i = 0; i < nHist; i++) {
//Log.d(TAG,"i="+i+", HR="+hrHistArr[i]);
xVals[i] = String.valueOf(i);
lineDataSet.addEntry(new Entry((float) histArr[i], i));
}
Log.d(TAG, "xVals=" + Arrays.toString(xVals) + ", lneDataSet=" + lineDataSet.toSimpleString());
lineDataSet.setColors(new int[]{0xffff0000});
LineData histLineData = new LineData(xVals, lineDataSet);
mLineChart.setData(histLineData);
mLineChart.getData().notifyDataChanged();
mLineChart.notifyDataSetChanged();
mLineChart.refreshDrawableState();
float xSpan = (nHist * 5.0f) / 60.0f; // time in minutes assuming one point every 5 seconds.
mLineChart.setDescription("Signal Strength History "
+ String.format("%.1f", xSpan)
+ " " + getString(R.string.minutes));
mLineChart.setDescriptionTextSize(12f);
mLineChart.invalidate();
//if (mConnection.mBound){
// lineChart.postInvalidate();
//}
}
} else {
Log.w(TAG,"not Bound to Server");
return;
}
}
}

View File

@@ -318,8 +318,10 @@ public class MainActivity2 extends AppCompatActivity {
return new FragmentHrAlg();
case 2:
return new FragmentSystem();
//case 3:
// return new FragmentBatt();
case 3:
return new FragmentWatchSig();
case 4:
return new FragmentBatt();
//case 4:
// return new FragmentDataSharing();
@@ -331,7 +333,7 @@ public class MainActivity2 extends AppCompatActivity {
@Override
public int getItemCount() {
return 3;
return 5;
}
}

View File

@@ -69,6 +69,7 @@ public class SdData implements Parcelable {
public CircBuf watchBattBuff = new CircBuf(24*3600/5, -1); // 24 hour buffer
public CircBuf phoneBattBuff = new CircBuf(24*3600/5, -1); // 24 hour buffer
public CircBuf watchSignalStrengthBuff = new CircBuf(4*3600/5, -1); // 4 hour buffer
/* Heart Rate Alarm Settings */
public boolean mHRAlarmActive = false;
@@ -136,12 +137,14 @@ public class SdData implements Parcelable {
public double mO2Sat = 0;
public double mPseizure = 0.;
public float watchSignalStrength;
public SdData() {
simpleSpec = new int[10];
rawData = new double[N_RAW_DATA];
rawData3D = new double[N_RAW_DATA * 3];
dataTime = new Time(Time.getCurrentTimezone());
dataTime.setToNow();
timeDiff = 0f;
}
@@ -162,8 +165,12 @@ public class SdData implements Parcelable {
// FIXME - this doesn't work!!!
Time tnow = new Time();
tnow.setToNow();
timeDiff = (tnow.toMillis(false)
- dataTime.toMillis(false))/1000f;
if (dataTime != null) {
timeDiff = (tnow.toMillis(false)
- dataTime.toMillis(false)) / 1000f;
} else {
timeDiff = 0f;
}
dataTime.setToNow();
Log.v(TAG, "fromJSON(): dataTime = " + dataTime.toString());
maxVal = jo.optInt("maxVal");
@@ -322,6 +329,7 @@ public class SdData implements Parcelable {
jsonObj.put("watchSdName", watchSdName);
jsonObj.put("watchFwVersion", watchFwVersion);
jsonObj.put("watchSdVersion", watchSdVersion);
jsonObj.put("watchSignalStrength", watchSignalStrength);
retval = jsonObj.toString();
} catch (Exception ex) {

View File

@@ -390,7 +390,6 @@ public abstract class SdDataSource {
mSamplePeriod = (short) dataObject.getInt("analysisPeriod");
mSampleFreq = (short) dataObject.getInt("sampleFreq");
mSdData.batteryPc = (short) dataObject.getInt("battery");
mSdData.watchBattBuff.add(mSdData.batteryPc);
Log.v(TAG, "updateFromJSON - mSamplePeriod=" + mSamplePeriod + " mSampleFreq=" + mSampleFreq);
mUtil.writeToSysLogFile("SDDataSource.updateFromJSON - Settings Received");
@@ -475,6 +474,7 @@ public abstract class SdDataSource {
// Update phone battery level - it is done here so it is called for all data sources.
mSdData.phoneBatteryPc = getPhoneBatteryLevel();
mSdData.phoneBattBuff.add(mSdData.phoneBatteryPc);
mSdData.watchBattBuff.add(mSdData.batteryPc);
try {
// FIXME - Use specified sampleFreq, not this hard coded one
mSampleFreq = 25;
@@ -538,8 +538,12 @@ public abstract class SdDataSource {
mSdData.roiPower = (long) roiPower / ACCEL_SCALE_FACTOR;
Time tnow = new Time();
tnow.setToNow();
mSdData.timeDiff = (tnow.toMillis(false)
- mSdData.dataTime.toMillis(false))/1000f;
if (mSdData.dataTime != null) {
mSdData.timeDiff = (tnow.toMillis(false)
- mSdData.dataTime.toMillis(false)) / 1000f;
} else {
mSdData.timeDiff = 0f;
}
mSdData.dataTime.setToNow();
mSdData.dataTime.setToNow();

View File

@@ -153,8 +153,8 @@ public class SdDataSourceBLE2 extends SdDataSource {
// FIXME: Read the shared preferences in this class so SdDataSource does not need to know
// FIXME: about BLE details.
Log.i(TAG, "mBLEDevice is " + mBleDeviceName + ", Addr=" + mBleDeviceAddr);
mSdData.watchSdName = mBleDeviceName;
mSdData.watchPartNo = mBleDeviceAddr;
//mSdData.watchSdName = mBleDeviceName;
mSdData.watchSerNo = mBleDeviceAddr;
boolean success = CurrentTimeService.startServer(mContext);
@@ -231,6 +231,8 @@ public class SdDataSourceBLE2 extends SdDataSource {
peripheral.setPreferredPhy(PhyType.LE_CODED, PhyType.LE_CODED, PhyOptions.S8);
peripheral.readPhy();
peripheral.readRemoteRssi();
boolean foundOsdService = false;
for (BluetoothGattService service : peripheral.getServices()) {
String servUuidStr = service.getUuid().toString();
@@ -420,6 +422,7 @@ public class SdDataSourceBLE2 extends SdDataSource {
mDataStatusTime = new Time(Time.getCurrentTimezone());
// Process the data to do seizure detection
doAnalysis();
mBlePeripheral.readRemoteRssi(); // Update RSSI
// Re-start collecting raw data.
nRawData = 0;
// Notify the device of the resulting alarm state
@@ -461,7 +464,8 @@ public class SdDataSourceBLE2 extends SdDataSource {
byte[] rawDataBytes = characteristic.getValue();
String watchSerNo = new String(rawDataBytes, StandardCharsets.UTF_8);
Log.i(TAG, "Received Watch Serial No.: " + watchSerNo);
mSdData.watchSerNo = watchSerNo;
//mSdData.watchSerNo = watchSerNo;
// We do not use this serial number because it is zero for PineTime - we set the MAC address at start-up instead.
} else if (charUuidStr.equals(CHAR_DEV_HW_VER)) {
byte[] rawDataBytes = characteristic.getValue();
String watchHwVer = new String(rawDataBytes, StandardCharsets.UTF_8);
@@ -490,6 +494,12 @@ public class SdDataSourceBLE2 extends SdDataSource {
Log.i(TAG, String.format("new MTU set: %d", mtu));
}
@Override
public void onReadRemoteRssi(@NotNull BluetoothPeripheral peripheral, int rssi, @NotNull GattStatus status) {
Log.d(TAG, String.format("Rssi = %d", rssi));
mSdData.watchSignalStrength = rssi;
mSdData.watchSignalStrengthBuff.add(rssi);
}
};

View File

@@ -821,15 +821,19 @@ public class SdServer extends Service implements SdDataReceiver {
// flag.
if (mFaultTimerCompleted) {
faultWarningBeep();
// Re-start the data source to see if that fixes it
Log.w(TAG,"FAULT - stopping data source");
mSdDataSource.stop();
mHandler.postDelayed(new Runnable() {
public void run() {
Log.w(TAG,"FAULT - restarting data source");
mSdDataSource.start();
}
}, 10000);
// Disable the data-source re-start for now because it was messing up BLE2 data source by ending up with multiple
// notifications for the same data when it reconnects.
if (false) {
// Re-start the data source to see if that fixes it
Log.w(TAG, "FAULT - stopping data source");
mSdDataSource.stop();
mHandler.postDelayed(new Runnable() {
public void run() {
Log.w(TAG, "FAULT - restarting data source");
mSdDataSource.start();
}
}, 10000);
}
} else {
startFaultTimer();
Log.v(TAG, "onSdDataFault() - starting Fault Timer");

View File

@@ -19,7 +19,7 @@
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/lineChart"
android:id="@+id/battLineChart"
android:layout_width="fill_parent"
android:layout_height="fill_parent">

View File

@@ -64,11 +64,6 @@
<TextView
android:id="@+id/fragment_watch_app_status_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="---" />
<TextView
android:id="@+id/battTv"
@@ -89,6 +84,140 @@
android:gravity="left"
android:text="---" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Watch Status"
android:textColor="@color/okTextColor"
android:background="@color/okBackgroundColor"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/okTextColor"
android:text="Watch Manufacturer: "/>
<TextView
android:id="@+id/watch_manuf_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="---"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/okTextColor"
android:text="Watch Part No: "/>
<TextView
android:id="@+id/watch_partno_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="---"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/okTextColor"
android:text="Watch FW Ver: "/>
<TextView
android:id="@+id/watch_fwver_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="---"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/okTextColor"
android:text="Watch App Name: "/>
<TextView
android:id="@+id/watch_sdname_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="---"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/okTextColor"
android:text="Watch App Ver: "/>
<TextView
android:id="@+id/watch_sdver_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="---"/>
</LinearLayout>
<TextView
android:id="@+id/fragment_watch_app_status_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="---" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/okTextColor"
android:text="Watch Battery Level: "/>
<TextView
android:id="@+id/watch_batt_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="---"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/okTextColor"
android:text="Watch Signal Strength: "/>
<TextView
android:id="@+id/watch_signal_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="---"/>
</LinearLayout>
</androidx.appcompat.widget.LinearLayoutCompat>

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FragmentOsdBaseClass">
<!-- TODO: Update blank fragment layout -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/okBackgroundColor"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Watch Signal Strength History"
android:textColor="@color/okTextColor"/>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Current Signal Strength: "
android:textColor="@color/okTextColor" />
<TextView
android:id="@+id/current_sig_strength_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="---"
android:textColor="@color/okTextColor" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" dB"
android:textColor="@color/okTextColor" />
</androidx.appcompat.widget.LinearLayoutCompat>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/sigStrengthLineChart"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
</com.github.mikephil.charting.charts.LineChart>
</FrameLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
</FrameLayout>

View File

@@ -2,7 +2,8 @@
<resources>
<string name="app_name">OpenSeizureDetector</string>
<string name="changelog">
"\nV4.2.4 - Fault alarm rather than crash if bluetooth system crashes.
"\nV4.2.6 - Fixed issues with Android 13 notifications and BLE data source shutdown. Added support for V2.x of Garmin watch app, Added new BLE2 data source, signal strength and battery level graphs.
\nV4.2.4 - Fault alarm rather than crash if bluetooth system crashes.
\nV4.2.3 - Bug Fixes (heart rate alarm and latched alarm issues)
\nV4.2 - Added support for PineTime Watches using Bluetooth data source.
\n - Added new, swipeable user interface to simplify the main screen..