diff --git a/assets/pool-water.jpg b/assets/pool-water.jpg new file mode 100644 index 0000000..1d8d6f2 Binary files /dev/null and b/assets/pool-water.jpg differ diff --git a/components/LiveTrainingView.js b/components/LiveTrainingView.js index b68bd5a..7dd881f 100644 --- a/components/LiveTrainingView.js +++ b/components/LiveTrainingView.js @@ -1,94 +1,47 @@ 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 { StyleSheet } from 'react-native'; +import { Button, Content, Text } 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'; +import { connect } from 'react-redux'; +import { stopSession } from '../state/ActionCreators'; +import backgroundColors from './Themes'; - -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) - }); - }); +function LiveTrainingView(props) +{ + const analysis = props.session.analysis; + const onStopClick = () => { + props.dispatch(stopSession()); + props.navigation.navigate('Home'); }; + const laps = (analysis.peaks.size / props.peaksPerLap).toFixed(1); + const totalMomentum = Math.trunc(analysis.totalMomentum / 10000); - 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 ( - - - - + return ( + + + + + {/* - - + + - + */} - + - - ); - } - + + ); } - -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', @@ -109,3 +62,14 @@ const styles = StyleSheet.create({ elevation: 1,*/ } }); + +const mapStateToProps = (state) => { + return { + session: state.session, + peaksPerLap: state.settings.peaksPerLap, + theme: state.settings.theme, + }; +}; + +export default connect(mapStateToProps)(LiveTrainingView); + diff --git a/components/ThemedStackNavigation.js b/components/ThemedStackNavigation.js index e04e941..214eb1a 100644 --- a/components/ThemedStackNavigation.js +++ b/components/ThemedStackNavigation.js @@ -26,7 +26,7 @@ function ThemedStackNavigation(props) { headerTitleStyle: { color: 'white', fontWeight: 'bold', - fontSize: "1.5em", + fontSize: 20, }, headerTintColor: "white", headerBackground: () => ( diff --git a/data_processing/DataProcessing.js b/data_processing/DataProcessing.js index bebc965..357733a 100644 --- a/data_processing/DataProcessing.js +++ b/data_processing/DataProcessing.js @@ -31,12 +31,11 @@ class DataProcessing { 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.session.running !== this.state.session.running) { + this.onRunningChanged(newState.session.running, newState.settings.deviceURL); }; if(newState.settings.peakDetectorSimpleThreshold !== this.state.settings.peakDetectorSimpleThreshold) { this.peakDetectorSimple = new PeakDetectorSimple(newState.settings.peakDetectorSimpleThreshold, this.onNewPeak); @@ -50,19 +49,21 @@ class DataProcessing { this.dataSource.stop(); 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) => { + let req = new XMLHttpRequest(); if (running) { - //console.log("Starting session"); - let req = new XMLHttpRequest(); + console.log("Starting session", deviceURL + "/api/session/start" ); req.open("GET", deviceURL + "/api/session/start"); + req.send(); this.dataSource.startIndex = 0; this.dataSource.start(); } else { - //console.log("Stopping session"); + console.log("Stopping session"); req.open("GET", deviceURL + "/api/session/stop"); + req.send(); this.dataSource.stop(); this.dataSource.startIndex = 0; } @@ -73,9 +74,10 @@ class DataProcessing { data.sessionStartTime; data.startIndex; 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 - this.rawMeasurements.concat(List(data.values)); + console.log("success: normal case"); + this.rawMeasurements = this.rawMeasurements.concat(List(data.values)); this.analyzeNewMeasurements(data.startIndex); success = true; } @@ -84,7 +86,12 @@ class DataProcessing { this.sessionStartTime = data.sessionStartTime; this.rawMeasurements = List(data.values); success = true; + console.log("New start", this.sessionStartTime, this.rawMeasurements.toArray()); } 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 this.dataSource.startIndex = 0; this.sessionStartTime = 0; @@ -94,21 +101,24 @@ class DataProcessing { if (success) { 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) => { - 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)); + console.log("new peaks", newPeaks, "total peaks", this.peaks.toArray()); 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 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 peakMaxWindow = windowedSeq.reduce((running, x) => Math.max(x, running), 0); const momentumWindow = windowedSeq.reduce((sum, x) => sum + x, 0); return { diff --git a/data_processing/DeviceDataSource.js b/data_processing/DeviceDataSource.js index 86aa5ff..6eb7b54 100644 --- a/data_processing/DeviceDataSource.js +++ b/data_processing/DeviceDataSource.js @@ -2,11 +2,12 @@ import * as msgpack from 'msgpack-lite'; class DeviceHttpDataSource { - constructor(dataUrl, onNewData, pollInterval=1000, startIndex = 0) { + constructor(dataUrl, onNewData, pollInterval=2000, startIndex = 0) { this.dataUrl = dataUrl; this.onNewData = onNewData; this.pollInterval = pollInterval; this.startIndex = startIndex; + this.timer = null; // msgpack setup this.msgpackCodec = msgpack.createCodec(); @@ -47,12 +48,13 @@ class DeviceHttpDataSource { //"values", "sessionStartTime", "startIndex" this.onNewData(decoded); } catch (err) { - //console.log(err); + console.log(err); } } start() { if (this.timer === null) { + console.log("Start monitoring"); this.timer = setInterval(this.fetchDataHttp, this.pollInterval); return true; } else { @@ -62,6 +64,7 @@ class DeviceHttpDataSource { stop() { if (this.timer !== null) { + console.log("stop monitoring"); clearInterval(this.timer); this.timer = null; return true; diff --git a/data_processing/PeakDetection.js b/data_processing/PeakDetection.js index d46d5ea..530cef7 100644 --- a/data_processing/PeakDetection.js +++ b/data_processing/PeakDetection.js @@ -8,12 +8,11 @@ * value is larger than (threshold + minimum_since_last_peak) */ class PeakDetectorSimple { - constructor(threshold, handleNewPeaks) { + constructor(threshold) { this._threshold = threshold; this._queue = []; this._last_min = 0; this._counter = 0; - this._handleNewPeaks = handleNewPeaks; } getThreshold() { @@ -22,14 +21,11 @@ class PeakDetectorSimple { addVector(vec) { let result = []; - const callbackBackup = this._handleNewPeaks; - this._handleNewPeaks = null; for (let i = 0; i < vec.length; ++i) { const res = this.add(vec[i]); if(res !== null) result.push(res); } - this._handleNewPeaks = callbackBackup; return result; } @@ -40,7 +36,7 @@ class PeakDetectorSimple { this._queue.shift(); } if (this._queue.length !== 3) { - return; + return null; } const [last, current, next] = this._queue; const is_maximum = current > next && current > last; diff --git a/state/ActionCreators.js b/state/ActionCreators.js index b345b7d..c641279 100644 --- a/state/ActionCreators.js +++ b/state/ActionCreators.js @@ -3,6 +3,7 @@ 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 STOP_SESSION = "STOP_SESSION"; export const reportDeviceData = (sessionId, newDataStart, data, analysis) => ({ type: NEW_DEVICE_DATA, @@ -24,4 +25,8 @@ export const changeTheme = newThemeName => ({ export const startSession = () => ({ type: START_SESSION +}) + +export const stopSession = () => ({ + type: STOP_SESSION }) \ No newline at end of file diff --git a/state/Reducer.js b/state/Reducer.js index d836eab..9344d83 100644 --- a/state/Reducer.js +++ b/state/Reducer.js @@ -1,16 +1,16 @@ import { combineReducers } from 'redux'; 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 = { theme: "hot", username: "", - deviceURL: "192.168.178.105", + deviceURL: "http://192.168.178.105", peaksPerLap: 30, // advanced peakDetector: 'SIMPLE', // either 'SIMPLE' or 'ZSCORE' - peakDetectorSimpleThreshold: 500, + peakDetectorSimpleThreshold: 2000, peakDetectorZScoreLag: 8, // peak detector z-score values peakDetectorZScoreThreshold: 2, @@ -53,18 +53,23 @@ const currentSessionReducer = (state = INITIAL_CURRENT_SESSION, action) => { rawData: List(), analysis: INITIAL_CURRENT_SESSION.analysis }; + case STOP_SESSION: + return { + running: false, + rawData: List(), + analysis: INITIAL_CURRENT_SESSION.analysis + }; case NEW_DEVICE_DATA: return { - running: action.data.length > 0, + running: action.data.size > 0, rawData: action.data, - analysis: { ...state.analysis, ...analysis }, + analysis: { ...state.analysis, ...action.analysis }, } default: return state } }; - export default combineReducers({ settings: settingsReducer, session: currentSessionReducer,