App does something :)
This commit is contained in:
parent
2584d2249f
commit
3cefa3fdbf
Binary file not shown.
After Width: | Height: | Size: 602 KiB |
|
@ -1,94 +1,47 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import { Container, Text, Header, Content, Left, Body, Right, Button, Icon, Title, Card, CardItem, Fab} from 'native-base';
|
import { Button, Content, Text } from 'native-base';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import IconCard from './IconCard';
|
import IconCard from './IconCard';
|
||||||
import Graph from './Graph';
|
import Graph from './Graph';
|
||||||
import DeviceHttpDataSource from './DeviceHttpDataSource';
|
import { connect } from 'react-redux';
|
||||||
import { PeakDetectorSimple } from '../data_processing/PeakDetection';
|
import { stopSession } from '../state/ActionCreators';
|
||||||
|
import backgroundColors from './Themes';
|
||||||
|
|
||||||
|
|
||||||
|
function LiveTrainingView(props)
|
||||||
export default class LiveTrainingView extends React.Component {
|
{
|
||||||
|
const analysis = props.session.analysis;
|
||||||
constructor(props) {
|
const onStopClick = () => {
|
||||||
super(props);
|
props.dispatch(stopSession());
|
||||||
this.state = {
|
props.navigation.navigate('Home');
|
||||||
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)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
const laps = (analysis.peaks.size / props.peaksPerLap).toFixed(1);
|
||||||
|
const totalMomentum = Math.trunc(analysis.totalMomentum / 10000);
|
||||||
|
|
||||||
handleStart = () => {
|
return (
|
||||||
fetch(this.config.deviceUrl + "/api/session/start").catch(err => console.log(err));
|
<LinearGradient
|
||||||
}
|
colors={backgroundColors[props.theme]}
|
||||||
|
start={[0, 0]}
|
||||||
handleStop = () => {
|
end={[0.5, 1]}
|
||||||
fetch(this.config.deviceUrl + "/api/session/stop").catch(err => console.log(err));
|
style={{ flex: 1 }}
|
||||||
}
|
>
|
||||||
|
<Content padder contentContainerStyle={{ justifyContent: 'space-around', flex: 1, paddingTop: 60 }}>
|
||||||
handleThemeChange = () => {
|
<IconCard label="BAHNEN" value={laps} iconName="retweet" iconType="AntDesign" />
|
||||||
this.setState((state, props) => { return { themeNumber: ((state.themeNumber + 1) % themeArray.length) } })
|
<IconCard label="ZÜGE" value={analysis.peaks.size} iconName="dashboard" iconType="AntDesign" />
|
||||||
}
|
<IconCard label="KRAFT" value={totalMomentum} iconName="ruler" iconType="Entypo" />
|
||||||
|
|
||||||
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() {
|
|
||||||
return (
|
|
||||||
<Content padder contentContainerStyle={{ justifyContent: 'space-around', flex: 1 }}>
|
|
||||||
<IconCard label="BAHNEN" value="9" iconName="retweet" iconType="AntDesign" fontSize={110} />
|
|
||||||
<IconCard label="ZÜGE" value="800" iconName="dashboard" iconType="AntDesign" />
|
|
||||||
<IconCard label="KRAFT" value="120" iconName="ruler" iconType="Entypo" />
|
|
||||||
{/*
|
{/*
|
||||||
<IconCard label="ZÜGE" value={this.state.numPeaks} iconName="dashboard" iconType="AntDesign" />
|
<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="BAHNEN" value={this.state.numLaps} iconName="retweet" iconType="AntDesign" />
|
||||||
|
|
||||||
<IconCard label="KRAFT" value="120" iconName="ruler" iconType="Entypo" />
|
<IconCard label="KRAFT" value="120" iconName="ruler" iconType="Entypo" />
|
||||||
*/}
|
*/}
|
||||||
|
<Button block secondary onPress={onStopClick}><Text>Stop</Text></Button>
|
||||||
</Content>
|
</Content>
|
||||||
|
</LinearGradient>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const backgroundColors = {
|
|
||||||
'hot': ['#830e5f', '#fd5139'],
|
|
||||||
'darkBlue': ['#4265a3', '#cfada7'],
|
|
||||||
'lightBlue': ['#50a4db', '#74bbe2'],
|
|
||||||
'foggy': ['#bc8db8', '#5d5e90'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const themeArray = [
|
|
||||||
'hot', 'darkBlue', 'lightBlue', 'foggy'
|
|
||||||
];
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
card: {
|
card: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
@ -109,3 +62,14 @@ const styles = StyleSheet.create({
|
||||||
elevation: 1,*/
|
elevation: 1,*/
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => {
|
||||||
|
return {
|
||||||
|
session: state.session,
|
||||||
|
peaksPerLap: state.settings.peaksPerLap,
|
||||||
|
theme: state.settings.theme,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(LiveTrainingView);
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ function ThemedStackNavigation(props) {
|
||||||
headerTitleStyle: {
|
headerTitleStyle: {
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontSize: "1.5em",
|
fontSize: 20,
|
||||||
},
|
},
|
||||||
headerTintColor: "white",
|
headerTintColor: "white",
|
||||||
headerBackground: () => (
|
headerBackground: () => (
|
||||||
|
|
|
@ -31,12 +31,11 @@ class DataProcessing {
|
||||||
|
|
||||||
onStateChange = () => {
|
onStateChange = () => {
|
||||||
const newState = this.store.getState();
|
const newState = this.store.getState();
|
||||||
//console.log("DataProcessing state change", this.state, newState);
|
|
||||||
if (newState.settings.deviceURL !== this.state.settings.deviceURL)
|
if (newState.settings.deviceURL !== this.state.settings.deviceURL)
|
||||||
this.onDataSourceChanged(newState.settings.deviceURL);
|
this.onDataSourceChanged(newState.settings.deviceURL);
|
||||||
|
|
||||||
if (newState.session.running && !this.state.session.running) {
|
if (newState.session.running !== this.state.session.running) {
|
||||||
this.onRunningChanged(newState.session.running);
|
this.onRunningChanged(newState.session.running, newState.settings.deviceURL);
|
||||||
};
|
};
|
||||||
if(newState.settings.peakDetectorSimpleThreshold !== this.state.settings.peakDetectorSimpleThreshold) {
|
if(newState.settings.peakDetectorSimpleThreshold !== this.state.settings.peakDetectorSimpleThreshold) {
|
||||||
this.peakDetectorSimple = new PeakDetectorSimple(newState.settings.peakDetectorSimpleThreshold, this.onNewPeak);
|
this.peakDetectorSimple = new PeakDetectorSimple(newState.settings.peakDetectorSimpleThreshold, this.onNewPeak);
|
||||||
|
@ -50,19 +49,21 @@ class DataProcessing {
|
||||||
this.dataSource.stop();
|
this.dataSource.stop();
|
||||||
this.dataSource = null;
|
this.dataSource = null;
|
||||||
}
|
}
|
||||||
this.dataSource = new DeviceHttpDataSource(this.newDeviceURL + "/api/session/data", this.onNewData);
|
this.dataSource = new DeviceHttpDataSource(newDeviceURL + "/api/session/data", this.onNewData);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRunningChanged = (running, deviceURL) => {
|
onRunningChanged = (running, deviceURL) => {
|
||||||
|
let req = new XMLHttpRequest();
|
||||||
if (running) {
|
if (running) {
|
||||||
//console.log("Starting session");
|
console.log("Starting session", deviceURL + "/api/session/start" );
|
||||||
let req = new XMLHttpRequest();
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
@ -73,9 +74,10 @@ class DataProcessing {
|
||||||
data.sessionStartTime;
|
data.sessionStartTime;
|
||||||
data.startIndex;
|
data.startIndex;
|
||||||
let success = false;
|
let success = false;
|
||||||
if (data.sessionStartTime === this.sessionStartTime && data.startIndex === this.rawMeasurements.length) {
|
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
|
||||||
this.rawMeasurements.concat(List(data.values));
|
console.log("success: normal case");
|
||||||
|
this.rawMeasurements = this.rawMeasurements.concat(List(data.values));
|
||||||
this.analyzeNewMeasurements(data.startIndex);
|
this.analyzeNewMeasurements(data.startIndex);
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
|
@ -84,7 +86,12 @@ 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("this.sessionStartTime", this.sessionStartTime);
|
||||||
|
console.log("this.rawMeasurements", this.rawMeasurements.toArray());
|
||||||
|
console.log("data", data);
|
||||||
// missed some data -> re-query
|
// missed some data -> re-query
|
||||||
this.dataSource.startIndex = 0;
|
this.dataSource.startIndex = 0;
|
||||||
this.sessionStartTime = 0;
|
this.sessionStartTime = 0;
|
||||||
|
@ -94,21 +101,24 @@ class DataProcessing {
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
const analysis = this.analyzeNewMeasurements(data.startIndex);
|
const analysis = this.analyzeNewMeasurements(data.startIndex);
|
||||||
this.store.dispatch(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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
analyzeNewMeasurements = (newDataStartIdx) => {
|
analyzeNewMeasurements = (newDataStartIdx) => {
|
||||||
const newPeaks = this.peakDetectorSimple.addVector(this.rawMeasurements.slice(newDataStartIdx));
|
//TODO is ".toArray()" really necessary here?
|
||||||
|
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) => max(x, running), 0);
|
const peakMax = this.rawMeasurements.reduce((running, x) => Math.max(x, running), 0);
|
||||||
|
|
||||||
// windowed quantities
|
// windowed quantities
|
||||||
const windowSizeMeasurements = WINDOW_SIZE_SECS * NUM_MEASUREMENTS_PER_SECOND;
|
const windowSizeMeasurements = WINDOW_SIZE_SECS * NUM_MEASUREMENTS_PER_SECOND;
|
||||||
const windowedSeq = this.rawMeasurements.slice(-windowSizeMeasurements);
|
const windowedSeq = this.rawMeasurements.slice(-windowSizeMeasurements);
|
||||||
const peakMaxWindow = windowedSeq.reduce((running, x) => max(x, running), 0);
|
const peakMaxWindow = windowedSeq.reduce((running, x) => Math.max(x, running), 0);
|
||||||
const momentumWindow = windowedSeq.reduce((sum, x) => sum + x, 0);
|
const momentumWindow = windowedSeq.reduce((sum, x) => sum + x, 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -2,11 +2,12 @@ import * as msgpack from 'msgpack-lite';
|
||||||
|
|
||||||
class DeviceHttpDataSource {
|
class DeviceHttpDataSource {
|
||||||
|
|
||||||
constructor(dataUrl, onNewData, pollInterval=1000, startIndex = 0) {
|
constructor(dataUrl, onNewData, pollInterval=2000, startIndex = 0) {
|
||||||
this.dataUrl = dataUrl;
|
this.dataUrl = dataUrl;
|
||||||
this.onNewData = onNewData;
|
this.onNewData = onNewData;
|
||||||
this.pollInterval = pollInterval;
|
this.pollInterval = pollInterval;
|
||||||
this.startIndex = startIndex;
|
this.startIndex = startIndex;
|
||||||
|
this.timer = null;
|
||||||
|
|
||||||
// msgpack setup
|
// msgpack setup
|
||||||
this.msgpackCodec = msgpack.createCodec();
|
this.msgpackCodec = msgpack.createCodec();
|
||||||
|
@ -47,12 +48,13 @@ class DeviceHttpDataSource {
|
||||||
//"values", "sessionStartTime", "startIndex"
|
//"values", "sessionStartTime", "startIndex"
|
||||||
this.onNewData(decoded);
|
this.onNewData(decoded);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
//console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (this.timer === null) {
|
if (this.timer === null) {
|
||||||
|
console.log("Start monitoring");
|
||||||
this.timer = setInterval(this.fetchDataHttp, this.pollInterval);
|
this.timer = setInterval(this.fetchDataHttp, this.pollInterval);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
@ -62,6 +64,7 @@ class DeviceHttpDataSource {
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (this.timer !== null) {
|
if (this.timer !== null) {
|
||||||
|
console.log("stop monitoring");
|
||||||
clearInterval(this.timer);
|
clearInterval(this.timer);
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -8,12 +8,11 @@
|
||||||
* value is larger than (threshold + minimum_since_last_peak)
|
* value is larger than (threshold + minimum_since_last_peak)
|
||||||
*/
|
*/
|
||||||
class PeakDetectorSimple {
|
class PeakDetectorSimple {
|
||||||
constructor(threshold, handleNewPeaks) {
|
constructor(threshold) {
|
||||||
this._threshold = threshold;
|
this._threshold = threshold;
|
||||||
this._queue = [];
|
this._queue = [];
|
||||||
this._last_min = 0;
|
this._last_min = 0;
|
||||||
this._counter = 0;
|
this._counter = 0;
|
||||||
this._handleNewPeaks = handleNewPeaks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getThreshold() {
|
getThreshold() {
|
||||||
|
@ -22,14 +21,11 @@ class PeakDetectorSimple {
|
||||||
|
|
||||||
addVector(vec) {
|
addVector(vec) {
|
||||||
let result = [];
|
let result = [];
|
||||||
const callbackBackup = this._handleNewPeaks;
|
|
||||||
this._handleNewPeaks = null;
|
|
||||||
for (let i = 0; i < vec.length; ++i) {
|
for (let i = 0; i < vec.length; ++i) {
|
||||||
const res = this.add(vec[i]);
|
const res = this.add(vec[i]);
|
||||||
if(res !== null)
|
if(res !== null)
|
||||||
result.push(res);
|
result.push(res);
|
||||||
}
|
}
|
||||||
this._handleNewPeaks = callbackBackup;
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +36,7 @@ class PeakDetectorSimple {
|
||||||
this._queue.shift();
|
this._queue.shift();
|
||||||
}
|
}
|
||||||
if (this._queue.length !== 3) {
|
if (this._queue.length !== 3) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
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;
|
||||||
|
|
|
@ -3,6 +3,7 @@ export const NEW_DEVICE_DATA = "NEW_DEVICE_DATA";
|
||||||
export const CHANGE_USER_NAME = "SET_USERNAME";
|
export const CHANGE_USER_NAME = "SET_USERNAME";
|
||||||
export const CHANGE_THEME = "CHANGE_THEME";
|
export const CHANGE_THEME = "CHANGE_THEME";
|
||||||
export const START_SESSION = "START_SESSION";
|
export const START_SESSION = "START_SESSION";
|
||||||
|
export const STOP_SESSION = "STOP_SESSION";
|
||||||
|
|
||||||
export const reportDeviceData = (sessionId, newDataStart, data, analysis) => ({
|
export const reportDeviceData = (sessionId, newDataStart, data, analysis) => ({
|
||||||
type: NEW_DEVICE_DATA,
|
type: NEW_DEVICE_DATA,
|
||||||
|
@ -24,4 +25,8 @@ export const changeTheme = newThemeName => ({
|
||||||
|
|
||||||
export const startSession = () => ({
|
export const startSession = () => ({
|
||||||
type: START_SESSION
|
type: START_SESSION
|
||||||
|
})
|
||||||
|
|
||||||
|
export const stopSession = () => ({
|
||||||
|
type: STOP_SESSION
|
||||||
})
|
})
|
|
@ -1,16 +1,16 @@
|
||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
import { List } from 'immutable';
|
import { List } from 'immutable';
|
||||||
import { CHANGE_THEME, CHANGE_USER_NAME, NEW_DEVICE_DATA, START_SESSION } from './ActionCreators';
|
import { CHANGE_THEME, CHANGE_USER_NAME, NEW_DEVICE_DATA, START_SESSION, STOP_SESSION } from './ActionCreators';
|
||||||
|
|
||||||
const INITIAL_SETTINGS = {
|
const INITIAL_SETTINGS = {
|
||||||
theme: "hot",
|
theme: "hot",
|
||||||
username: "",
|
username: "",
|
||||||
deviceURL: "192.168.178.105",
|
deviceURL: "http://192.168.178.105",
|
||||||
peaksPerLap: 30,
|
peaksPerLap: 30,
|
||||||
|
|
||||||
// advanced
|
// advanced
|
||||||
peakDetector: 'SIMPLE', // either 'SIMPLE' or 'ZSCORE'
|
peakDetector: 'SIMPLE', // either 'SIMPLE' or 'ZSCORE'
|
||||||
peakDetectorSimpleThreshold: 500,
|
peakDetectorSimpleThreshold: 2000,
|
||||||
|
|
||||||
peakDetectorZScoreLag: 8, // peak detector z-score values
|
peakDetectorZScoreLag: 8, // peak detector z-score values
|
||||||
peakDetectorZScoreThreshold: 2,
|
peakDetectorZScoreThreshold: 2,
|
||||||
|
@ -53,18 +53,23 @@ const currentSessionReducer = (state = INITIAL_CURRENT_SESSION, action) => {
|
||||||
rawData: List(),
|
rawData: List(),
|
||||||
analysis: INITIAL_CURRENT_SESSION.analysis
|
analysis: INITIAL_CURRENT_SESSION.analysis
|
||||||
};
|
};
|
||||||
|
case STOP_SESSION:
|
||||||
|
return {
|
||||||
|
running: false,
|
||||||
|
rawData: List(),
|
||||||
|
analysis: INITIAL_CURRENT_SESSION.analysis
|
||||||
|
};
|
||||||
case NEW_DEVICE_DATA:
|
case NEW_DEVICE_DATA:
|
||||||
return {
|
return {
|
||||||
running: action.data.length > 0,
|
running: action.data.size > 0,
|
||||||
rawData: action.data,
|
rawData: action.data,
|
||||||
analysis: { ...state.analysis, ...analysis },
|
analysis: { ...state.analysis, ...action.analysis },
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
settings: settingsReducer,
|
settings: settingsReducer,
|
||||||
session: currentSessionReducer,
|
session: currentSessionReducer,
|
||||||
|
|
Loading…
Reference in New Issue