app changes
This commit is contained in:
parent
d0b81bb7fb
commit
2584d2249f
Binary file not shown.
After Width: | Height: | Size: 419 KiB |
|
@ -1,48 +1,58 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import msgpack5 from 'msgpack5';
|
import * as msgpack from 'msgpack-lite';
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
config = {
|
|
||||||
peakDetector: 'simple',
|
|
||||||
peakDetectorConfig: {'threshold': 3000},
|
|
||||||
|
|
||||||
deviceUrl: 'http://smartswim',
|
|
||||||
peaksPerLap: 30,
|
|
||||||
forceAverage
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
class DeviceHttpDataSource extends React.Component {
|
class DeviceHttpDataSource extends React.Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.data = [];
|
this.data = [];
|
||||||
this.dataUrl = new URL("/api/session/data", this.props.deviceUrl);
|
this.dataUrl = this.props.deviceUrl + "/api/session/data";
|
||||||
|
|
||||||
// msgpack setup
|
// msgpack setup
|
||||||
this.msgpack = msgpack5();
|
this.msgpackCodec = msgpack.createCodec();
|
||||||
this.msgpack.registerDecoder(205, function (byteArr) {
|
this.msgpackCodec.addExtUnpacker(205, function (byteArr) {
|
||||||
const buffer = byteArr.buffer.slice(byteArr.byteOffset, byteArr.byteLength + byteArr.byteOffset);
|
const buffer = byteArr.buffer.slice(byteArr.byteOffset, byteArr.byteLength + byteArr.byteOffset);
|
||||||
return new Int16Array(buffer);
|
const result = new Int16Array(buffer);
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.fetchDataHttp = this.fetchDataHttp.bind(this);
|
this.fetchDataHttp = this.fetchDataHttp.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getUrl(url) {
|
||||||
|
return new Promise((accept, reject) => {
|
||||||
|
var req = new XMLHttpRequest();
|
||||||
|
req.open("GET", url, true);
|
||||||
|
req.responseType = "arraybuffer";
|
||||||
|
|
||||||
|
req.onload = function (event) {
|
||||||
|
var resp = req.response;
|
||||||
|
if (resp) {
|
||||||
|
accept(resp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.send(null);
|
||||||
|
});
|
||||||
|
//todo reject on error
|
||||||
|
}
|
||||||
|
|
||||||
async fetchDataHttp() {
|
async fetchDataHttp() {
|
||||||
this.dataUrl.searchParams.set("startIdx", this.data.length);
|
try {
|
||||||
const response = await fetch(this.dataUrl);
|
const url = this.dataUrl + "?startIdx=" + this.data.length;
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await this.getUrl(url);
|
||||||
const decoded = this.msgpack.decode(arrayBuffer);
|
const decoded = msgpack.decode(new Uint8Array(arrayBuffer), { codec: this.msgpackCodec });
|
||||||
const typedValueArr = decoded['values'];
|
|
||||||
console.log("new data inside fetch", typedValueArr);
|
const typedValueArr = decoded['values'];
|
||||||
const newDataStart = this.data.length;
|
const newDataStart = this.data.length;
|
||||||
for (let i = 0; i < typedValueArr.length; ++i) {
|
for (let i = 0; i < typedValueArr.length; ++i) {
|
||||||
this.data.push(typedValueArr[i]);
|
this.data.push(typedValueArr[i]);
|
||||||
|
}
|
||||||
|
this.props.onNewData(this.data, newDataStart);
|
||||||
|
} catch (err) {
|
||||||
|
//console.log(err);
|
||||||
}
|
}
|
||||||
this.props.onNewData(this.data, newDataStart);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -55,7 +65,7 @@ class DeviceHttpDataSource extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return "";
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {View, StyleSheet, Text} from 'react-native';
|
import {View, StyleSheet, Text} from 'react-native';
|
||||||
|
|
||||||
import Svg, {Polyline, Polygon, Rect, G} from 'react-native-svg-web';
|
//import Svg, {Polyline, Polygon, Rect, G} from 'react-native-svg-web';
|
||||||
|
import Svg, {Polyline, Polygon, Rect, G} from 'react-native-svg';
|
||||||
|
|
||||||
|
|
||||||
const Graph = props => {
|
const Graph = props => {
|
||||||
|
@ -11,7 +12,6 @@ const Graph = props => {
|
||||||
|
|
||||||
const coordStr = data.map((element, i) => `${i}, ${element / 2}`);
|
const coordStr = data.map((element, i) => `${i}, ${element / 2}`);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||||
<Svg height={graphHeight} width="80%" viewbox="0 0 100 100">
|
<Svg height={graphHeight} width="80%" viewbox="0 0 100 100">
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Content, Card, CardItem, Body, Text, Button } from 'native-base';
|
||||||
|
import { Image, ScrollView } from 'react-native';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { startSession} from '../state/ActionCreators';
|
||||||
|
|
||||||
|
function HomeView(props) {
|
||||||
|
const buttonText = props.running ? "View Swim Session" : "Start swimming";
|
||||||
|
|
||||||
|
const onButtonPress = () => {
|
||||||
|
if(!props.running) {
|
||||||
|
props.dispatch(startSession());
|
||||||
|
}
|
||||||
|
props.navigation.navigate('Training')
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Content padder contentContainerStyle={{ justifyContent: 'space-around', flex: 1, marginTop: 70 }}>
|
||||||
|
<ScrollView styles={{ marginHorizontal: 20, paddingTop: 100 }} alwaysBounceHorizontal={true} alwaysBounceVertical={true}>
|
||||||
|
<Card style={{ backgroundColor: "transparent" }}>
|
||||||
|
<CardItem cardBody>
|
||||||
|
<Image source={require('../assets/pool-water.jpg')} style={{ height: 100, width: null, flex: 1 }} />
|
||||||
|
</CardItem>
|
||||||
|
|
||||||
|
<CardItem style={{backgroundColor: 'rgba(255, 255, 255, 0.6)'}}>
|
||||||
|
<Body>
|
||||||
|
<Button block onPress={onButtonPress}>
|
||||||
|
<Text>{buttonText}</Text>
|
||||||
|
</Button>
|
||||||
|
</Body>
|
||||||
|
</CardItem>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card style={{ backgroundColor: "transparent" }}>
|
||||||
|
<CardItem cardBody>
|
||||||
|
<Image source={require('../assets/blue-water-background.jpg')} style={{ height: 100, width: null, flex: 1 }} />
|
||||||
|
</CardItem>
|
||||||
|
|
||||||
|
<CardItem style={{backgroundColor: 'rgba(255, 255, 255, 0.6)'}}>
|
||||||
|
<Body>
|
||||||
|
<Button block >
|
||||||
|
<Text>Settings</Text>
|
||||||
|
</Button>
|
||||||
|
</Body>
|
||||||
|
</CardItem>
|
||||||
|
</Card>
|
||||||
|
</ScrollView>
|
||||||
|
</Content>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => {
|
||||||
|
return { running: state.session.running };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(HomeView);
|
|
@ -6,13 +6,14 @@ const IconCard = props => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<View style={{ alignItems: 'center', justifyContent: 'center', paddingLeft: 20 }}>
|
|
||||||
|
<View style={{ paddingLeft: 20 }}>
|
||||||
|
<Text style={{ color: 'white', fontSize: props.fontSize, textAlign: "center" }}> {props.value}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ alignItems: 'center', justifyContent: 'center', paddingLeft: 20 }}>
|
||||||
<Icon style={{ color: 'white', fontSize: 40 }} name={props.iconName} type={props.iconType} />
|
<Icon style={{ color: 'white', fontSize: 40 }} name={props.iconName} type={props.iconType} />
|
||||||
<Text style={{ color: 'white', marginTop: 5 }}> {props.label}</Text>
|
<Text style={{ color: 'white', marginTop: 5 }}> {props.label}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ paddingRight: 20 }}>
|
|
||||||
<Text style={{ color: 'white', fontSize: props.fontSize }}> {props.value}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -29,7 +30,8 @@ const styles = StyleSheet.create({
|
||||||
});
|
});
|
||||||
|
|
||||||
IconCard.defaultProps = {
|
IconCard.defaultProps = {
|
||||||
fontSize: 85
|
fontSize: 85,
|
||||||
|
flex: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
export default IconCard;
|
export default IconCard;
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { View, StyleSheet } from 'react-native';
|
||||||
|
import { Container, Text, Header, Content, Left, Body, Right, Button, Icon, Title, Card, CardItem, Fab} from 'native-base';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
import IconCard from './IconCard';
|
||||||
|
import Graph from './Graph';
|
||||||
|
import DeviceHttpDataSource from './DeviceHttpDataSource';
|
||||||
|
import { PeakDetectorSimple } from '../data_processing/PeakDetection';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default class LiveTrainingView extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
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)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleStart = () => {
|
||||||
|
fetch(this.config.deviceUrl + "/api/session/start").catch(err => console.log(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStop = () => {
|
||||||
|
fetch(this.config.deviceUrl + "/api/session/stop").catch(err => console.log(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
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="BAHNEN" value={this.state.numLaps} iconName="retweet" iconType="AntDesign" />
|
||||||
|
|
||||||
|
<IconCard label="KRAFT" value="120" iconName="ruler" iconType="Entypo" />
|
||||||
|
*/}
|
||||||
|
|
||||||
|
</Content>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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,*/
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,70 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
|
import { BlurView } from 'expo-blur';
|
||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
import backgroundColors from './Themes';
|
||||||
|
|
||||||
|
// Own views
|
||||||
|
import LiveTrainingView from './LiveTrainingView';
|
||||||
|
import HomeView from './HomeView';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
|
||||||
|
const Stack = createStackNavigator();
|
||||||
|
|
||||||
|
|
||||||
|
function ThemedStackNavigation(props) {
|
||||||
|
|
||||||
|
const screenOptions = {
|
||||||
|
cardStyle: {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
opacity: 1
|
||||||
|
},
|
||||||
|
headerTransparent: "true",
|
||||||
|
headerTitleStyle: {
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: "1.5em",
|
||||||
|
},
|
||||||
|
headerTintColor: "white",
|
||||||
|
headerBackground: () => (
|
||||||
|
<BlurView
|
||||||
|
tint="dark"
|
||||||
|
intensity={30}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LinearGradient
|
||||||
|
colors={backgroundColors[props.themeName]}
|
||||||
|
start={[0, 0]}
|
||||||
|
end={[0.5, 1]}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
<NavigationContainer>
|
||||||
|
<Stack.Navigator initialRouteName="Home">
|
||||||
|
<Stack.Screen
|
||||||
|
name="Home"
|
||||||
|
component={HomeView}
|
||||||
|
options={{ ...screenOptions, headerTitle: "SwimTracker" }}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="Training"
|
||||||
|
component={LiveTrainingView}
|
||||||
|
options={{ ...screenOptions, headerTitle: "Training" }}
|
||||||
|
/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
</NavigationContainer>
|
||||||
|
</LinearGradient>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => {
|
||||||
|
return { themeName: state.settings.theme };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(ThemedStackNavigation);
|
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
const backgroundColors = {
|
||||||
|
'hot': ['#830e5f', '#fd5139'],
|
||||||
|
'darkBlue': ['#4265a3', '#cfada7'],
|
||||||
|
'lightBlue': ['#50a4db', '#74bbe2'],
|
||||||
|
'foggy': ['#bc8db8', '#5d5e90'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default backgroundColors;
|
|
@ -0,0 +1,129 @@
|
||||||
|
import DeviceHttpDataSource from './DeviceDataSource';
|
||||||
|
import { List } from 'immutable';
|
||||||
|
import { reportDeviceData } from '../state/ActionCreators';
|
||||||
|
import { PeakDetectorSimple} from './PeakDetection';
|
||||||
|
|
||||||
|
|
||||||
|
// todo: put in settings?
|
||||||
|
const NUM_MEASUREMENTS_PER_SECOND = 10;
|
||||||
|
const WINDOW_SIZE_SECS = 5;
|
||||||
|
|
||||||
|
// This is the main data processing entry point, coupling between device and redux store
|
||||||
|
// - periodically fetch data
|
||||||
|
// - feeds them to analysis, (manages analysis classes)
|
||||||
|
// - adds them to redux store
|
||||||
|
class DataProcessing {
|
||||||
|
constructor(reduxStore) {
|
||||||
|
this.store = reduxStore;
|
||||||
|
this.store.subscribe(this.onStateChange);
|
||||||
|
this.state = this.store.getState();
|
||||||
|
this.dataSource = null;
|
||||||
|
//console.log("state", this.state);
|
||||||
|
console.assert(this.state.session.running === false, "Created DataProcessing with running=True");
|
||||||
|
this.onDataSourceChanged(this.state.settings.deviceURL);
|
||||||
|
|
||||||
|
this.rawMeasurements = List();
|
||||||
|
this.sessionStartTime = 0;
|
||||||
|
|
||||||
|
this.peakDetectorSimple = new PeakDetectorSimple(this.state.settings.peakDetectorSimpleThreshold);
|
||||||
|
this.peaks = List();
|
||||||
|
}
|
||||||
|
|
||||||
|
onStateChange = () => {
|
||||||
|
const newState = this.store.getState();
|
||||||
|
//console.log("DataProcessing state change", this.state, newState);
|
||||||
|
if (newState.settings.deviceURL !== this.state.settings.deviceURL)
|
||||||
|
this.onDataSourceChanged(newState.settings.deviceURL);
|
||||||
|
|
||||||
|
if (newState.session.running && !this.state.session.running) {
|
||||||
|
this.onRunningChanged(newState.session.running);
|
||||||
|
};
|
||||||
|
if(newState.settings.peakDetectorSimpleThreshold !== this.state.settings.peakDetectorSimpleThreshold) {
|
||||||
|
this.peakDetectorSimple = new PeakDetectorSimple(newState.settings.peakDetectorSimpleThreshold, this.onNewPeak);
|
||||||
|
this.peaks = List(this.peakDetectorSimple.addVector(this.rawMeasurements));
|
||||||
|
};
|
||||||
|
this.state = newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDataSourceChanged = (newDeviceURL) => {
|
||||||
|
if (this.dataSource !== null) {
|
||||||
|
this.dataSource.stop();
|
||||||
|
this.dataSource = null;
|
||||||
|
}
|
||||||
|
this.dataSource = new DeviceHttpDataSource(this.newDeviceURL + "/api/session/data", this.onNewData);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRunningChanged = (running, deviceURL) => {
|
||||||
|
if (running) {
|
||||||
|
//console.log("Starting session");
|
||||||
|
let req = new XMLHttpRequest();
|
||||||
|
req.open("GET", deviceURL + "/api/session/start");
|
||||||
|
this.dataSource.startIndex = 0;
|
||||||
|
this.dataSource.start();
|
||||||
|
} else {
|
||||||
|
//console.log("Stopping session");
|
||||||
|
req.open("GET", deviceURL + "/api/session/stop");
|
||||||
|
this.dataSource.stop();
|
||||||
|
this.dataSource.startIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNewData = (data) => {
|
||||||
|
data.values;
|
||||||
|
data.sessionStartTime;
|
||||||
|
data.startIndex;
|
||||||
|
let success = false;
|
||||||
|
if (data.sessionStartTime === this.sessionStartTime && data.startIndex === this.rawMeasurements.length) {
|
||||||
|
// normal case, add received data to measurement array
|
||||||
|
this.rawMeasurements.concat(List(data.values));
|
||||||
|
this.analyzeNewMeasurements(data.startIndex);
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else if (data.startIndex === 0) {
|
||||||
|
// new start
|
||||||
|
this.sessionStartTime = data.sessionStartTime;
|
||||||
|
this.rawMeasurements = List(data.values);
|
||||||
|
success = true;
|
||||||
|
} else {
|
||||||
|
// missed some data -> re-query
|
||||||
|
this.dataSource.startIndex = 0;
|
||||||
|
this.sessionStartTime = 0;
|
||||||
|
//console.log("Problem: got non-consequtive data. Received:", data,
|
||||||
|
// "Own state ", { startTime: this.sessionStartTime, values: this.rawMeasurements });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
const analysis = this.analyzeNewMeasurements(data.startIndex);
|
||||||
|
this.store.dispatch(reportDeviceData(this.sessionStartTime, data.startIndex, this.rawMeasurements, analysis));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
analyzeNewMeasurements = (newDataStartIdx) => {
|
||||||
|
const newPeaks = this.peakDetectorSimple.addVector(this.rawMeasurements.slice(newDataStartIdx));
|
||||||
|
this.peaks = this.peaks.concat(List(newPeaks));
|
||||||
|
const totalMomentum = this.rawMeasurements.reduce((sum, x) => sum + x, 0);
|
||||||
|
const peakMax = this.rawMeasurements.reduce((running, x) => max(x, running), 0);
|
||||||
|
|
||||||
|
// windowed quantities
|
||||||
|
const windowSizeMeasurements = WINDOW_SIZE_SECS * NUM_MEASUREMENTS_PER_SECOND;
|
||||||
|
const windowedSeq = this.rawMeasurements.slice(-windowSizeMeasurements);
|
||||||
|
const peakMaxWindow = windowedSeq.reduce((running, x) => max(x, running), 0);
|
||||||
|
const momentumWindow = windowedSeq.reduce((sum, x) => sum + x, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
peaks: this.peaks,
|
||||||
|
totalTime: this.rawMeasurements.length / NUM_MEASUREMENTS_PER_SECOND,
|
||||||
|
activeTime: 0,
|
||||||
|
totalMomentum: totalMomentum,
|
||||||
|
peakFrequency: 0,
|
||||||
|
peakMax: peakMax,
|
||||||
|
// windowed quantities
|
||||||
|
momentumWindow: momentumWindow,
|
||||||
|
frequencyWindow: 0,
|
||||||
|
peakMaxWindow: peakMaxWindow,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataProcessing;
|
|
@ -0,0 +1,74 @@
|
||||||
|
import * as msgpack from 'msgpack-lite';
|
||||||
|
|
||||||
|
class DeviceHttpDataSource {
|
||||||
|
|
||||||
|
constructor(dataUrl, onNewData, pollInterval=1000, startIndex = 0) {
|
||||||
|
this.dataUrl = dataUrl;
|
||||||
|
this.onNewData = onNewData;
|
||||||
|
this.pollInterval = pollInterval;
|
||||||
|
this.startIndex = startIndex;
|
||||||
|
|
||||||
|
// msgpack setup
|
||||||
|
this.msgpackCodec = msgpack.createCodec();
|
||||||
|
this.msgpackCodec.addExtUnpacker(205, function (byteArr) {
|
||||||
|
const buffer = byteArr.buffer.slice(byteArr.byteOffset, byteArr.byteLength + byteArr.byteOffset);
|
||||||
|
const result = new Int16Array(buffer);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fetchDataHttp = this.fetchDataHttp.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUrl(url) {
|
||||||
|
return new Promise((accept, reject) => {
|
||||||
|
var req = new XMLHttpRequest();
|
||||||
|
req.open("GET", url, true);
|
||||||
|
req.responseType = "arraybuffer";
|
||||||
|
|
||||||
|
req.onload = function (event) {
|
||||||
|
var resp = req.response;
|
||||||
|
if (resp) {
|
||||||
|
accept(resp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.addEventListener("error", evt => reject(evt));
|
||||||
|
req.addEventListener("abort", evt => reject(evt));
|
||||||
|
|
||||||
|
req.send(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchDataHttp() {
|
||||||
|
try {
|
||||||
|
const url = this.dataUrl + "?startIdx=" + this.startIndex;
|
||||||
|
const arrayBuffer = await this.getUrl(url);
|
||||||
|
const decoded = msgpack.decode(new Uint8Array(arrayBuffer), { codec: this.msgpackCodec });
|
||||||
|
this.startIndex += decoded["values"].length;
|
||||||
|
//"values", "sessionStartTime", "startIndex"
|
||||||
|
this.onNewData(decoded);
|
||||||
|
} catch (err) {
|
||||||
|
//console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.timer === null) {
|
||||||
|
this.timer = setInterval(this.fetchDataHttp, this.pollInterval);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.timer !== null) {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeviceHttpDataSource;
|
|
@ -9,7 +9,6 @@
|
||||||
*/
|
*/
|
||||||
class PeakDetectorSimple {
|
class PeakDetectorSimple {
|
||||||
constructor(threshold, handleNewPeaks) {
|
constructor(threshold, handleNewPeaks) {
|
||||||
this.peaks = [];
|
|
||||||
this._threshold = threshold;
|
this._threshold = threshold;
|
||||||
this._queue = [];
|
this._queue = [];
|
||||||
this._last_min = 0;
|
this._last_min = 0;
|
||||||
|
@ -17,20 +16,25 @@ class PeakDetectorSimple {
|
||||||
this._handleNewPeaks = handleNewPeaks;
|
this._handleNewPeaks = handleNewPeaks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getThreshold() {
|
||||||
|
return this._threshold;
|
||||||
|
}
|
||||||
|
|
||||||
addVector(vec) {
|
addVector(vec) {
|
||||||
|
let result = [];
|
||||||
const callbackBackup = this._handleNewPeaks;
|
const callbackBackup = this._handleNewPeaks;
|
||||||
const numPeaksBefore = this.peaks.length;
|
|
||||||
this._handleNewPeaks = null;
|
this._handleNewPeaks = null;
|
||||||
for (let i = 0; i < vec.length; ++i) {
|
for (let i = 0; i < vec.length; ++i) {
|
||||||
this.add(vec[i]);
|
const res = this.add(vec[i]);
|
||||||
|
if(res !== null)
|
||||||
|
result.push(res);
|
||||||
}
|
}
|
||||||
this._handleNewPeaks = callbackBackup;
|
this._handleNewPeaks = callbackBackup;
|
||||||
if (numPeaksBefore != this.peaks.length) {
|
return result;
|
||||||
this._handleNewPeaks(this.peaks);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
add(value) {
|
add(value) {
|
||||||
|
let result = null;
|
||||||
this._queue.push(value);
|
this._queue.push(value);
|
||||||
if (this._queue.length > 3) {
|
if (this._queue.length > 3) {
|
||||||
this._queue.shift();
|
this._queue.shift();
|
||||||
|
@ -41,14 +45,11 @@ 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) {
|
||||||
this.peaks.push(this._counter);
|
result = this._counter;
|
||||||
this._last_min = current;
|
|
||||||
if (this._handleNewPeaks) {
|
|
||||||
this._handleNewPeaks(this.peaks);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this._last_min = Math.min(this._last_min, current);
|
this._last_min = Math.min(this._last_min, current);
|
||||||
this._counter += 1;
|
this._counter += 1;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
|
||||||
|
export const NEW_DEVICE_DATA = "NEW_DEVICE_DATA";
|
||||||
|
export const CHANGE_USER_NAME = "SET_USERNAME";
|
||||||
|
export const CHANGE_THEME = "CHANGE_THEME";
|
||||||
|
export const START_SESSION = "START_SESSION";
|
||||||
|
|
||||||
|
export const reportDeviceData = (sessionId, newDataStart, data, analysis) => ({
|
||||||
|
type: NEW_DEVICE_DATA,
|
||||||
|
sessionId: sessionId,
|
||||||
|
newDataStart: newDataStart,
|
||||||
|
data: data,
|
||||||
|
analysis: analysis,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const changeUsername = newUsername => ({
|
||||||
|
type: CHANGE_USER_NAME,
|
||||||
|
newUserName: newUsername,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const changeTheme = newThemeName => ({
|
||||||
|
type: CHANGE_THEME,
|
||||||
|
newThemeName: newThemeName
|
||||||
|
})
|
||||||
|
|
||||||
|
export const startSession = () => ({
|
||||||
|
type: START_SESSION
|
||||||
|
})
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import { List } from 'immutable';
|
||||||
|
import { CHANGE_THEME, CHANGE_USER_NAME, NEW_DEVICE_DATA, START_SESSION } from './ActionCreators';
|
||||||
|
|
||||||
|
const INITIAL_SETTINGS = {
|
||||||
|
theme: "hot",
|
||||||
|
username: "",
|
||||||
|
deviceURL: "192.168.178.105",
|
||||||
|
peaksPerLap: 30,
|
||||||
|
|
||||||
|
// advanced
|
||||||
|
peakDetector: 'SIMPLE', // either 'SIMPLE' or 'ZSCORE'
|
||||||
|
peakDetectorSimpleThreshold: 500,
|
||||||
|
|
||||||
|
peakDetectorZScoreLag: 8, // peak detector z-score values
|
||||||
|
peakDetectorZScoreThreshold: 2,
|
||||||
|
peakDetectorZScoreInfluence: 0.1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const INITIAL_CURRENT_SESSION = {
|
||||||
|
running: false,
|
||||||
|
rawData: List(),
|
||||||
|
analysis: {
|
||||||
|
'peaks': List(),
|
||||||
|
'totalTime': null,
|
||||||
|
'activeTime': null,
|
||||||
|
'totalMomentum': null,
|
||||||
|
'peakFrequency': null,
|
||||||
|
'peakMax': null,
|
||||||
|
// windowed quantities
|
||||||
|
'momentumWindow': null,
|
||||||
|
'frequencyWindow': null,
|
||||||
|
'peakMaxWindow': null,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const settingsReducer = (state = INITIAL_SETTINGS, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case CHANGE_THEME:
|
||||||
|
return { ...state, theme: action.newThemeName };
|
||||||
|
case CHANGE_USER_NAME:
|
||||||
|
return { ...state, username: action.newUsername };
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentSessionReducer = (state = INITIAL_CURRENT_SESSION, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case START_SESSION:
|
||||||
|
return {
|
||||||
|
running: true,
|
||||||
|
rawData: List(),
|
||||||
|
analysis: INITIAL_CURRENT_SESSION.analysis
|
||||||
|
};
|
||||||
|
case NEW_DEVICE_DATA:
|
||||||
|
return {
|
||||||
|
running: action.data.length > 0,
|
||||||
|
rawData: action.data,
|
||||||
|
analysis: { ...state.analysis, ...analysis },
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default combineReducers({
|
||||||
|
settings: settingsReducer,
|
||||||
|
session: currentSessionReducer,
|
||||||
|
});
|
Loading…
Reference in New Issue