App: peak detection, basic graph works

This commit is contained in:
Martin Bauer 2020-05-17 15:57:26 +02:00
parent 2e0baca485
commit d0b81bb7fb
4 changed files with 349 additions and 56 deletions

131
App.js
View File

@ -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 <AppLoading />;
}
const handleNewData = data => {
console.log("new data callback", data);
};
return (
<Container>
<DeviceHttpDataSource deviceUrl="http://smartswim" onNewData={handleNewData}></DeviceHttpDataSource>
<DeviceHttpDataSource deviceUrl={this.config.deviceUrl}
onNewData={this.handleNewData}
pollInterval={this.config.updateInterval}></DeviceHttpDataSource>
<LinearGradient
colors={backgroundColors[themeArray[this.state.themeNumber]]}
start={[0, 0]}
end={[0.5, 1]}
style={{flex: 1}}
style={{ flex: 1 }}
>
<Header transparent>
<Body>
<Title style={{color: 'white'}}>TRAINING LÄUFT</Title>
</Body>
<Right>
<Button transparent>
<Text style={{color: 'white'}}>STOP</Text>
</Button>
<Header transparent>
<Body>
<Title style={{ color: 'white' }}>TRAINING LÄUFT</Title>
</Body>
<Right>
<Button transparent onPress={this.handleStart}>
<Icon style={{ color: 'white' }} name="play" type="FontAwesome5" />
</Button>
<Button transparent onPress={this.handleThemeChange.bind(this)}>
<Icon style={{color: 'white'}} name="paint-brush" type="FontAwesome5"/>
</Button>
<Button transparent onPress={this.handleStop}>
<Icon style={{ color: 'white' }} name="stop" type="FontAwesome5" />
</Button>
</Right>
</Header>
<Button transparent onPress={this.handleThemeChange.bind(this)}>
<Icon style={{ color: 'white' }} name="paint-brush" type="FontAwesome5" />
</Button>
</Right>
</Header>
<Content padder contentContainerStyle={{justifyContent: 'space-around', flex: 1}}>
<IconCard label="ZÜGE" value="1234" iconName="dashboard" iconType="AntDesign" />
<IconCard label="BAHNEN" value="42" iconName="retweet" iconType="AntDesign" />
<IconCard label="KRAFT" value="120" iconName="ruler" iconType="Entypo" />
<IconCard label="ZEIT" value="20:12" iconName="clock" iconType="Feather" fontSize={55} />
{/*<Graph></Graph> */}
</Content>
<Content padder contentContainerStyle={{ justifyContent: 'space-around', flex: 1 }}>
<IconCard label="ZÜGE" value={this.state.numPeaks} iconName="dashboard" iconType="AntDesign" />
<IconCard label="BAHNEN" value={this.state.numLaps} iconName="retweet" iconType="AntDesign" />
{/*
<IconCard label="KRAFT" value="120" iconName="ruler" iconType="Entypo" />
<IconCard label="ZEIT" value="20:12" iconName="clock" iconType="Feather" fontSize={55} /> */}
<Graph data={this.state.measurements}></Graph>
</Content>
</LinearGradient>
@ -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,
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,*/
}
elevation: 1,*/
}
});

View File

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

View File

@ -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 data = props.data.slice(-600);
const coordStr = data.map((element, i) => `${i*2}, ${element}`);
const coordStr = data.map((element, i) => `${i}, ${element / 2}`);
return (

View File

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