App, peak detection bugfix

This commit is contained in:
Martin Bauer 2020-06-04 18:46:15 +02:00
parent 3cefa3fdbf
commit 0b0c3f8e36
9 changed files with 5299 additions and 2539 deletions

136
App.js
View File

@ -1,14 +1,19 @@
import React from 'react'; import React from 'react';
import { View, StyleSheet } from 'react-native';
import { AppLoading } from 'expo'; 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 { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient'; import * as Font from 'expo-font';
import IconCard from './components/IconCard';
import Graph from './components/Graph'; // Redux
import DeviceHttpDataSource from './components/DeviceHttpDataSource'; import swimtrackerReducer from './state/Reducer';
import { PeakDetectorSimple } from './data-analysis/PeakDetection'; import { createStore } from 'redux';
import { Provider } from 'react-redux';
import ThemedStackNavigation from './components/ThemedStackNavigation';
import DataProcessing from "./data_processing/DataProcessing";
const store = createStore(swimtrackerReducer);
const dataProcessing = new DataProcessing(store);
export default class App extends React.Component { export default class App extends React.Component {
@ -16,26 +21,7 @@ export default class App extends React.Component {
super(props); super(props);
this.state = { this.state = {
isReady: false, 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() { async componentDidMount() {
@ -47,106 +33,16 @@ export default class App extends React.Component {
this.setState({ isReady: true }); 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) } })
}
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() { render() {
if (!this.state.isReady) { if (!this.state.isReady) {
return <AppLoading />; return <AppLoading />;
} }
return ( return (
<Container> <Provider store={store}>
<DeviceHttpDataSource deviceUrl={this.config.deviceUrl} <ThemedStackNavigation/>
onNewData={this.handleNewData} </Provider>
pollInterval={this.config.updateInterval}></DeviceHttpDataSource>
<LinearGradient
colors={backgroundColors[themeArray[this.state.themeNumber]]}
start={[0, 0]}
end={[0.5, 1]}
style={{ flex: 1 }}
>
<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.handleStop}>
<Icon style={{ color: 'white' }} name="stop" type="FontAwesome5" />
</Button>
<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={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>
</Container>
); );
} }
} }
const backgroundColors = {
'hot': ['#830e5f', '#fd5139'],
'darkBlue': ['#4265a3', '#cfada7'],
'lightBlue': ['#50a4db', '#74bbe2'],
'foggy': ['#bc8db8', '#5d5e90'],
};
const themeArray = [
'hot', 'darkBlue', 'lightBlue', 'foggy'
];
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,*/
}
});

View File

@ -3,7 +3,6 @@
"name": "swimtrainer-app", "name": "swimtrainer-app",
"slug": "swimtrainer-app", "slug": "swimtrainer-app",
"privacy": "public", "privacy": "public",
"sdkVersion": "34.0.0",
"platforms": [ "platforms": [
"ios", "ios",
"android", "android",
@ -25,6 +24,7 @@
], ],
"ios": { "ios": {
"supportsTablet": true "supportsTablet": true
} },
"description": ""
} }
} }

View File

@ -57,16 +57,17 @@ class DataProcessing {
if (running) { if (running) {
console.log("Starting session", deviceURL + "/api/session/start" ); console.log("Starting session", deviceURL + "/api/session/start" );
req.open("GET", deviceURL + "/api/session/start"); req.open("GET", deviceURL + "/api/session/start");
req.send();
this.dataSource.startIndex = 0; this.dataSource.startIndex = 0;
this.dataSource.start(); this.dataSource.start();
} else { } else {
console.log("Stopping session"); console.log("Stopping session");
req.open("GET", deviceURL + "/api/session/stop"); req.open("GET", deviceURL + "/api/session/stop");
req.send();
this.dataSource.stop(); this.dataSource.stop();
this.dataSource.startIndex = 0; this.dataSource.startIndex = 0;
} }
req.addEventListener("error", evt => console.log(evt));
req.addEventListener("abort", evt => console.log(evt));
req.send();
} }
onNewData = (data) => { onNewData = (data) => {
@ -76,9 +77,7 @@ class DataProcessing {
let success = false; let success = false;
if (data.sessionStartTime == this.sessionStartTime && data.startIndex == this.rawMeasurements.size) { if (data.sessionStartTime == this.sessionStartTime && data.startIndex == this.rawMeasurements.size) {
// normal case, add received data to measurement array // normal case, add received data to measurement array
console.log("success: normal case");
this.rawMeasurements = this.rawMeasurements.concat(List(data.values)); this.rawMeasurements = this.rawMeasurements.concat(List(data.values));
this.analyzeNewMeasurements(data.startIndex);
success = true; success = true;
} }
else if (data.startIndex === 0) { else if (data.startIndex === 0) {
@ -86,7 +85,6 @@ class DataProcessing {
this.sessionStartTime = data.sessionStartTime; this.sessionStartTime = data.sessionStartTime;
this.rawMeasurements = List(data.values); this.rawMeasurements = List(data.values);
success = true; success = true;
console.log("New start", this.sessionStartTime, this.rawMeasurements.toArray());
} else { } else {
console.log("Requery :("); console.log("Requery :(");
console.log("this.sessionStartTime", this.sessionStartTime); console.log("this.sessionStartTime", this.sessionStartTime);
@ -102,7 +100,6 @@ class DataProcessing {
if (success) { if (success) {
const analysis = this.analyzeNewMeasurements(data.startIndex); const analysis = this.analyzeNewMeasurements(data.startIndex);
const report = reportDeviceData(this.sessionStartTime, data.startIndex, this.rawMeasurements, analysis); const report = reportDeviceData(this.sessionStartTime, data.startIndex, this.rawMeasurements, analysis);
console.log("reporting device data", report);
this.store.dispatch(report); this.store.dispatch(report);
} }
} }
@ -111,7 +108,6 @@ class DataProcessing {
//TODO is ".toArray()" really necessary here? //TODO is ".toArray()" really necessary here?
const newPeaks = this.peakDetectorSimple.addVector(this.rawMeasurements.slice(newDataStartIdx).toArray()); const newPeaks = this.peakDetectorSimple.addVector(this.rawMeasurements.slice(newDataStartIdx).toArray());
this.peaks = this.peaks.concat(List(newPeaks)); this.peaks = this.peaks.concat(List(newPeaks));
console.log("new peaks", newPeaks, "total peaks", this.peaks.toArray());
const totalMomentum = this.rawMeasurements.reduce((sum, x) => sum + x, 0); const totalMomentum = this.rawMeasurements.reduce((sum, x) => sum + x, 0);
const peakMax = this.rawMeasurements.reduce((running, x) => Math.max(x, running), 0); const peakMax = this.rawMeasurements.reduce((running, x) => Math.max(x, running), 0);

View File

@ -2,7 +2,7 @@ import * as msgpack from 'msgpack-lite';
class DeviceHttpDataSource { class DeviceHttpDataSource {
constructor(dataUrl, onNewData, pollInterval=2000, startIndex = 0) { constructor(dataUrl, onNewData, pollInterval=800, startIndex = 0) {
this.dataUrl = dataUrl; this.dataUrl = dataUrl;
this.onNewData = onNewData; this.onNewData = onNewData;
this.pollInterval = pollInterval; this.pollInterval = pollInterval;

View File

@ -41,7 +41,8 @@ class PeakDetectorSimple {
const [last, current, next] = this._queue; const [last, current, next] = this._queue;
const is_maximum = current > next && current > last; const is_maximum = current > next && current > last;
if (is_maximum && (current - this._last_min) > this._threshold) { if (is_maximum && (current - this._last_min) > this._threshold) {
result = this._counter; result = this._counter + 1;
this._last_min = current;
} }
this._last_min = Math.min(this._last_min, current); this._last_min = Math.min(this._last_min, current);
this._counter += 1; this._counter += 1;

View File

@ -0,0 +1,11 @@
import { PeakDetectorSimple } from './PeakDetection';
describe("test PeakDetectorSimple", () => {
it("detects simple peak", () => {
let pd = new PeakDetectorSimple(40);
const result = pd.addVector([0, 10, 30, 50, 30, 50, 2, 0, 60, 0 ]);
expect(result).toEqual([3, 8]);
});
});

7624
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,24 +5,46 @@
"android": "expo start --android", "android": "expo start --android",
"ios": "expo start --ios", "ios": "expo start --ios",
"web": "expo start --web", "web": "expo start --web",
"eject": "expo eject" "eject": "expo eject",
"test": "jest"
},
"jest": {
"preset": "jest-expo",
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)"
]
}, },
"dependencies": { "dependencies": {
"expo": "^34.0.1", "@react-native-community/masked-view": "0.1.6",
"expo-linear-gradient": "^6.0.0", "@react-navigation/native": "^5.4.2",
"@react-navigation/stack": "^5.3.9",
"expo": "^37.0.0",
"expo-blur": "~8.1.0",
"expo-linear-gradient": "~8.1.0",
"immutable": "^4.0.0-rc.12",
"msgpack-lite": "^0.1.26",
"msgpack5": "^4.2.1", "msgpack5": "^4.2.1",
"native-base": "2.13.8", "native-base": "2.13.8",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "16.8.3", "react": "16.9.0",
"react-dom": "^16.13.1", "react-dom": "16.9.0",
"react-native": "https://github.com/expo/react-native/archive/sdk-34.0.0.tar.gz", "react-native": "0.61.4",
"react-native-chart-kit": "^3.13.0", "react-native-chart-kit": "^3.13.0",
"react-native-svg": "^9.13.6", "react-native-gesture-handler": "~1.6.0",
"react-native-unimodules": "^0.5.4", "react-native-reanimated": "~1.7.0",
"react-native-web": "^0.11.4" "react-native-safe-area-context": "0.7.3",
"react-native-screens": "~2.2.0",
"react-native-svg": "11.0.1",
"react-native-svg-web": "^1.0.7",
"react-native-unimodules": "~0.8.1",
"react-native-web": "^0.11.7",
"react-redux": "^7.2.0",
"redux": "^4.0.5"
}, },
"devDependencies": { "devDependencies": {
"babel-preset-expo": "^6.0.0" "babel-preset-expo": "^8.1.0",
"jest-expo": "^37.0.0",
"react-test-renderer": "^16.13.1"
}, },
"private": true "private": true
} }

View File

@ -5,12 +5,12 @@ import { CHANGE_THEME, CHANGE_USER_NAME, NEW_DEVICE_DATA, START_SESSION, STOP_SE
const INITIAL_SETTINGS = { const INITIAL_SETTINGS = {
theme: "hot", theme: "hot",
username: "", username: "",
deviceURL: "http://192.168.178.105", deviceURL: "http://192.168.178.110",
peaksPerLap: 30, peaksPerLap: 30,
// advanced // advanced
peakDetector: 'SIMPLE', // either 'SIMPLE' or 'ZSCORE' peakDetector: 'SIMPLE', // either 'SIMPLE' or 'ZSCORE'
peakDetectorSimpleThreshold: 2000, peakDetectorSimpleThreshold: 4000,
peakDetectorZScoreLag: 8, // peak detector z-score values peakDetectorZScoreLag: 8, // peak detector z-score values
peakDetectorZScoreThreshold: 2, peakDetectorZScoreThreshold: 2,