diff --git a/App.js b/App.js
index 7a0733f..8af5922 100644
--- a/App.js
+++ b/App.js
@@ -1,13 +1,14 @@
import React from 'react';
-import {View, StyleSheet} from 'react-native';
+import { View, StyleSheet } from 'react-native';
import { AppLoading } from 'expo';
import { Container, Text, Header, Content, Left, Body, Right, Button, Icon, Title, Card, CardItem } from 'native-base';
import * as Font from 'expo-font';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import IconCard from './components/IconCard';
-/*import Graph from './components/Graph';*/
+import Graph from './components/Graph';
import DeviceHttpDataSource from './components/DeviceHttpDataSource';
+import { PeakDetectorSimple } from './data-analysis/PeakDetection';
export default class App extends React.Component {
@@ -16,7 +17,25 @@ export default class App extends React.Component {
this.state = {
isReady: false,
themeNumber: 0,
+ numPeaks: 0,
+ numLaps: 0,
+ measurements: []
};
+
+ this.config = {
+ deviceUrl: "http://smartswim",
+ peakThreshold: 30,
+ peaksPerLap: 30,
+ updateInterval : 3000,
+ };
+
+ this.peakDetector = new PeakDetectorSimple(this.config.peakThreshold, peaks => {
+ console.log("peaks:", peaks.length);
+ this.setState({
+ numPeaks: peaks.length,
+ numLaps: (peaks.length / this.config.peaksPerLap).toFixed(1)
+ });
+ });
}
async componentDidMount() {
@@ -28,51 +47,69 @@ export default class App extends React.Component {
this.setState({ isReady: true });
}
+ handleStart = () => {
+ fetch(new URL("/api/session/start", this.config.deviceUrl));
+ }
+
+ handleStop = () => {
+ fetch(new URL("/api/session/stop", this.config.deviceUrl));
+ }
+
handleThemeChange = () => {
- this.setState( (state, props) => {return {themeNumber: ((state.themeNumber + 1) % themeArray.length) }})
- };
+ this.setState((state, props) => { return { themeNumber: ((state.themeNumber + 1) % themeArray.length) } })
+ }
+
+ handleNewData = (fullData, newDataStart) => {
+ const newData = fullData.slice(newDataStart);
+ console.log("New data", newData.length, "Full data", fullData.length, "new data start", newDataStart);
+ console.log("new data", newData);
+ this.peakDetector.addVector(newData);
+ this.setState({measurements: fullData});
+ }
render() {
if (!this.state.isReady) {
return ;
}
-
- const handleNewData = data => {
- console.log("new data callback", data);
- };
return (
-
+
-
-
- TRAINING LÄUFT
-
-
-
+
+
+ TRAINING LÄUFT
+
+
+
-
+
-
-
+
+
+
-
-
-
-
-
- {/* */}
-
+
+
+
+ {/*
+
+ */}
+
+
@@ -94,22 +131,22 @@ const themeArray = [
];
const styles = StyleSheet.create({
- card: {
- flexDirection: 'row',
- backgroundColor: 'rgba(0, 0, 0, 0.2)',
- margin: 5,
- padding: 5,
- borderRadius: 3,
- justifyContent: 'space-between',
- /*
- shadowColor: "#000",
- shadowOffset: {
- width: 0,
- height: 1,
- },
- shadowOpacity: 0.18,
- shadowRadius: 1.00,
-
- elevation: 1,*/
- }
+ card: {
+ flexDirection: 'row',
+ backgroundColor: 'rgba(0, 0, 0, 0.2)',
+ margin: 5,
+ padding: 5,
+ borderRadius: 3,
+ justifyContent: 'space-between',
+ /*
+ shadowColor: "#000",
+ shadowOffset: {
+ width: 0,
+ height: 1,
+ },
+ shadowOpacity: 0.18,
+ shadowRadius: 1.00,
+
+ elevation: 1,*/
+ }
});
diff --git a/components/DeviceHttpDataSource.js b/components/DeviceHttpDataSource.js
new file mode 100644
index 0000000..72697c4
--- /dev/null
+++ b/components/DeviceHttpDataSource.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import msgpack5 from 'msgpack5';
+
+
+/*
+config = {
+ peakDetector: 'simple',
+ peakDetectorConfig: {'threshold': 3000},
+
+ deviceUrl: 'http://smartswim',
+ peaksPerLap: 30,
+ forceAverage
+}
+*/
+
+class DeviceHttpDataSource extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.data = [];
+ this.dataUrl = new URL("/api/session/data", this.props.deviceUrl);
+
+ // msgpack setup
+ this.msgpack = msgpack5();
+ this.msgpack.registerDecoder(205, function (byteArr) {
+ const buffer = byteArr.buffer.slice(byteArr.byteOffset, byteArr.byteLength + byteArr.byteOffset);
+ return new Int16Array(buffer);
+ });
+
+ this.fetchDataHttp = this.fetchDataHttp.bind(this);
+ }
+
+ async fetchDataHttp() {
+ this.dataUrl.searchParams.set("startIdx", this.data.length);
+ const response = await fetch(this.dataUrl);
+ const arrayBuffer = await response.arrayBuffer();
+ const decoded = this.msgpack.decode(arrayBuffer);
+ const typedValueArr = decoded['values'];
+ console.log("new data inside fetch", typedValueArr);
+ const newDataStart = this.data.length;
+ for (let i = 0; i < typedValueArr.length; ++i) {
+ this.data.push(typedValueArr[i]);
+ }
+ this.props.onNewData(this.data, newDataStart);
+ }
+
+ componentDidMount() {
+ this.timer = setInterval(this.fetchDataHttp, this.props.pollInterval);
+ }
+
+ componentWillUnmount() {
+ clearInterval(this.timer);
+ this.timer = null;
+ }
+
+ render() {
+ return "";
+ }
+}
+
+
+DeviceHttpDataSource.propTypes = {
+ deviceUrl: PropTypes.string.isRequired,
+ onNewData: PropTypes.func.isRequired,
+ pollInterval: PropTypes.number // poll interval in ms
+};
+
+DeviceHttpDataSource.defaultProps = {
+ pollInterval: 20000
+};
+
+export default DeviceHttpDataSource;
\ No newline at end of file
diff --git a/components/Graph.js b/components/Graph.js
index f079419..4111e55 100644
--- a/components/Graph.js
+++ b/components/Graph.js
@@ -1,18 +1,15 @@
import React from 'react';
import {View, StyleSheet, Text} from 'react-native';
-import Svg, {Polyline, Polygon, Rect, G} from 'react-native-svg';
+import Svg, {Polyline, Polygon, Rect, G} from 'react-native-svg-web';
const Graph = props => {
const graphHeight = 100;
- const data = [];
- for(let i=0; i < 300; ++i) {
- data.push( Math.random() * 100);
- }
-
- const coordStr = data.map((element, i) => `${i*2}, ${element}`);
+ const data = props.data.slice(-600);
+
+ const coordStr = data.map((element, i) => `${i}, ${element / 2}`);
return (
@@ -25,7 +22,7 @@ const Graph = props => {
strokeWidth="3"
strokeOpacity="0.5"
strokeLinejoin="round"
- fill="none"
+ fill="none"
/>
diff --git a/data-analysis/PeakDetection.js b/data-analysis/PeakDetection.js
new file mode 100644
index 0000000..caf6242
--- /dev/null
+++ b/data-analysis/PeakDetection.js
@@ -0,0 +1,186 @@
+
+/**
+ * A simple peak detector
+ *
+ * Usage: Successively add values via add() and query the indices of the peaks with peaks
+ *
+ * A peak is detected if the current point is local maximum (no filtering!) and the current
+ * value is larger than (threshold + minimum_since_last_peak)
+ */
+class PeakDetectorSimple {
+ constructor(threshold, handleNewPeaks) {
+ this.peaks = [];
+ this._threshold = threshold;
+ this._queue = [];
+ this._last_min = 0;
+ this._counter = 0;
+ this._handleNewPeaks = handleNewPeaks;
+ }
+
+ addVector(vec) {
+ const callbackBackup = this._handleNewPeaks;
+ const numPeaksBefore = this.peaks.length;
+ this._handleNewPeaks = null;
+ for (let i = 0; i < vec.length; ++i) {
+ this.add(vec[i]);
+ }
+ this._handleNewPeaks = callbackBackup;
+ if (numPeaksBefore != this.peaks.length) {
+ this._handleNewPeaks(this.peaks);
+ }
+ }
+
+ add(value) {
+ this._queue.push(value);
+ if (this._queue.length > 3) {
+ this._queue.shift();
+ }
+ if (this._queue.length !== 3) {
+ return;
+ }
+ const [last, current, next] = this._queue;
+ const is_maximum = current > next && current > last;
+ if (is_maximum && (current - this._last_min) > this._threshold) {
+ this.peaks.push(this._counter);
+ this._last_min = current;
+ if (this._handleNewPeaks) {
+ this._handleNewPeaks(this.peaks);
+ }
+ }
+ this._last_min = Math.min(this._last_min, current);
+ this._counter += 1;
+ }
+}
+
+
+/**
+ * Implementation of Z-Score peak detection according to
+ * https://stackoverflow.com/questions/22583391/peak-signal-detection-in-realtime-timeseries-data
+ */
+class PeakDetectorZScore {
+ constructor(lag, threshold, influence, handleNewPeaks) {
+ this.peaks = [];
+ this._filter = ZScoreFilter(lag, threshold, influence);
+ this._counter = 0;
+ this._previous_signal = 0;
+ this._max = null;
+ this._max_index = null;
+ this._handleNewPeaks = handleNewPeaks;
+ }
+
+ addVector(vec) {
+ const callbackBackup = this._handleNewPeaks;
+ const numPeaksBefore = this.peaks.length;
+ this._handleNewPeaks = null;
+ for (let i = 0; i < vec.length; ++i) {
+ this.add(vec[i]);
+ }
+ this._handleNewPeaks = callbackBackup;
+ if (numPeaksBefore != this.peaks.length) {
+ this._handleNewPeaks(this.peaks);
+ }
+ }
+
+ add(value) {
+ const signal = this._filter.add(value);
+ if (signal != null) {
+ const rising_flank = this._previous_signal !== 1 && signal === 1;
+ const falling_flank = this._previous_signal === 1 && signal !== 1;
+ if (rising_flank)
+ this._max = -1;
+ if (signal === 1 && this._max != null && value > this._max) {
+ this._max = value;
+ this._max_index = this._counter;
+ }
+ if (falling_flank) {
+ this.peaks.push(this._max_index);
+ if (this._handleNewPeaks) {
+ this._handleNewPeaks(this.peaks);
+ }
+ }
+
+ this._previous_signal = signal;
+ }
+ this._counter += 1;
+ }
+}
+
+
+export { PeakDetectorSimple, PeakDetectorZScore };
+
+
+// --------------------------------------- Helper classes -------------------------------------------------------------
+
+class StatisticsQueue {
+ constructor(size) {
+ this._size = size;
+
+ this._queue = [];
+ this._queue_sum = 0; // running sum over all elements currently in _queue
+ this._queue_sum_sq = 0; // sum of squared elements in _queue
+ }
+
+ add(value) {
+ this._queue.push(value);
+
+ this._queue_sum += value;
+ this._queue_sum_sq += value * value;
+
+ if (this._queue.length > this._size) {
+ const removed = this._queue[0];
+ this._queue.shift();
+ this._queue_sum -= removed;
+ this._queue_sum_sq -= removed * removed;
+ }
+ }
+
+ get avg() {
+ return this._queue_sum / self._queue.length;
+ }
+
+ get variance() {
+ const exp_sq = this._queue_sum_sq / self._queue.length;
+ const my_avg = self.avg;
+ return exp_sq - (my_avg * my_avg);
+ }
+
+ get std_deviation() {
+ return Math.sqrt(this.variance);
+ }
+
+ get filled() {
+ return this._queue.length === this._size;
+ }
+}
+
+class ZScoreFilter {
+
+ constructor(lag, threshold, influence) {
+ this._threshold = threshold;
+ this._influence = influence;
+
+ this._last_value = null;
+ this._stat_queue = StatisticsQueue(lag);
+ }
+
+ add(value) {
+ let sq = this._stat_queue;
+ if (!sq.filled) {
+ sq.add(value);
+ this._last_value = value;
+ return null;
+ } else {
+ const avg = sq.avg;
+ if (Math.abs(value - avg) > this._threshold * sq.std_deviation) {
+ const signal = value > avg ? 1 : -1;
+ const filtered = this._influence * value + (1 - this._influence) * this._last_value;
+ sq.add(filtered);
+ this._last_value = filtered;
+ return signal;
+ } else {
+ sq.add(value);
+ this._last_value = value;
+ }
+ }
+ }
+}