App, peak detection bugfix
This commit is contained in:
parent
3cefa3fdbf
commit
0b0c3f8e36
136
App.js
136
App.js
|
@ -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,*/
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
4
app.json
4
app.json
|
@ -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": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
|
@ -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
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue