New setup from scratch - all modules updated - app now in subfolder
This commit is contained in:
80
SwimTracker/data_processing/DataAnalysis.js
Normal file
80
SwimTracker/data_processing/DataAnalysis.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { PeakDetectorSimple } from './PeakDetection';
|
||||
import { MovingAverage} from './MovingAverage';
|
||||
import { List } from 'immutable';
|
||||
|
||||
|
||||
export default class DataAnalysis {
|
||||
|
||||
constructor() {
|
||||
this._resetCache(null, 0);
|
||||
}
|
||||
|
||||
analyze(analysisParameters, sessionId, allMeasurements) {
|
||||
const cacheValid = (
|
||||
this.sessionId === sessionId &&
|
||||
this.analyzedUpToIdx <= allMeasurements.size &&
|
||||
this.analysisParameters === analysisParameters);
|
||||
|
||||
let newData = null;
|
||||
|
||||
if (cacheValid) {
|
||||
newData = allMeasurements.slice(this.analyzedUpToIdx);
|
||||
}
|
||||
else {
|
||||
this._resetCache(analysisParameters, sessionId);
|
||||
newData = allMeasurements;
|
||||
console.log("cache reset");
|
||||
}
|
||||
const allMeasurementsSize = allMeasurements.size ? allMeasurements.size : allMeasurements.length;
|
||||
const newDataArr = (typeof newData.toArray ==="function") ? newData.toArray() : newData;
|
||||
|
||||
// active time
|
||||
const newAverages = this.movingAverage.addVector(newDataArr);
|
||||
this.activeMeasurements += newAverages.reduce((n, val) => {
|
||||
return n + ((val >= analysisParameters.activeTimeThreshold) ? 1 : 0);
|
||||
}, 0);
|
||||
|
||||
// peaks
|
||||
const newPeaks = this.peakDetectorSimple.addVector(newDataArr);
|
||||
this.allPeaks = this.allPeaks.concat(List(newPeaks));
|
||||
|
||||
// aggregated sum/max
|
||||
this.aggregatedMomentum = newData.reduce((sum, x) => sum + x, this.aggregatedMomentum);
|
||||
this.peakMax = newData.reduce((running, x) => Math.max(x, running), this.peakMax);
|
||||
|
||||
// windowed
|
||||
const windowNumDataPoints = analysisParameters.windowSizeInSecs * analysisParameters.numMeasurementsPerSec;
|
||||
const windowed = allMeasurements.slice(-windowNumDataPoints);
|
||||
const peakMaxWindow = windowed.reduce((running, x) => Math.max(x, running), 0);
|
||||
const momentumWindow = windowed.reduce((sum, x) => sum + x, 0);
|
||||
|
||||
|
||||
this.analyzedUpToIdx = allMeasurementsSize;
|
||||
return {
|
||||
peaks: this.allPeaks,
|
||||
totalTime: allMeasurementsSize / analysisParameters.numMeasurementsPerSec,
|
||||
activeTime: this.activeMeasurements / analysisParameters.numMeasurementsPerSec,
|
||||
|
||||
totalMomentum: this.aggregatedMomentum,
|
||||
peakMax: this.peakMax,
|
||||
|
||||
momentumWindow: momentumWindow,
|
||||
peakMaxWindow: peakMaxWindow,
|
||||
};
|
||||
}
|
||||
|
||||
_resetCache(analysisParameters, sessionId) {
|
||||
this.movingAverage = analysisParameters ? new MovingAverage(analysisParameters.movingAverageWindowSize) : null;
|
||||
this.activeMeasurements = 0;
|
||||
|
||||
this.peakDetectorSimple = analysisParameters ? new PeakDetectorSimple(analysisParameters.peakDetectorSimpleThreshold) : null;
|
||||
this.allPeaks = List();
|
||||
|
||||
this.aggregatedMomentum = 0;
|
||||
this.peakMax = 0;
|
||||
|
||||
this.sessionId = sessionId;
|
||||
this.analyzedUpToIdx = 0;
|
||||
this.analysisParameters = analysisParameters;
|
||||
}
|
||||
};
|
||||
128
SwimTracker/data_processing/DataProcessing.js
Normal file
128
SwimTracker/data_processing/DataProcessing.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import DeviceHttpDataSource from './DeviceDataSource';
|
||||
import { List } from 'immutable';
|
||||
import { reportDeviceData, resetDeviceData } from '../state/ActionCreators';
|
||||
import { PeakDetectorSimple } from './PeakDetection';
|
||||
|
||||
|
||||
// todo: put in settings?
|
||||
const NUM_MEASUREMENTS_PER_SECOND = 10;
|
||||
const WINDOW_SIZE_SECS = 5;
|
||||
|
||||
|
||||
class DataProcessing {
|
||||
constructor(reduxStore) {
|
||||
this.store = reduxStore;
|
||||
this.store.subscribe(this.onStateChange);
|
||||
this.state = this.store.getState();
|
||||
this.dataSource = null;
|
||||
this.peakDetectorSimple = new PeakDetectorSimple(this.state.settings.peakDetectorSimpleThreshold);
|
||||
|
||||
this.onDataSourceChanged(this.state.settings.deviceURL);
|
||||
this.onRunningChanged(this.state.session.running, this.state.settings.deviceURL);
|
||||
}
|
||||
|
||||
onStateChange = () => {
|
||||
const newState = this.store.getState();
|
||||
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, newState.settings.deviceURL);
|
||||
};
|
||||
if (newState.settings.peakDetectorSimpleThreshold !== this.state.settings.peakDetectorSimpleThreshold) {
|
||||
this.onAnalysisParameterChange();
|
||||
};
|
||||
this.state = newState;
|
||||
}
|
||||
|
||||
resetAnalysis = () => {
|
||||
this.peakDetectorSimple = new PeakDetectorSimple(this.state.settings.peakDetectorSimpleThreshold);
|
||||
}
|
||||
|
||||
onAnalysisParameterChange = () => {
|
||||
this.resetAnalysis();
|
||||
this.peakDetectorSimple.addVector(this.state.session.rawData.toArray());
|
||||
const analysis = this.analyzeNewMeasurements(data.values, List());
|
||||
this.store.dispatch(reportDeviceData(this.state.session.sessionId, this.state.session.rawData.size, this.state.session.rawData, analysis));
|
||||
}
|
||||
|
||||
onDataSourceChanged = (newDeviceURL) => {
|
||||
if (this.dataSource !== null) {
|
||||
this.dataSource.stop();
|
||||
this.dataSource = null;
|
||||
}
|
||||
this.dataSource = new DeviceHttpDataSource(newDeviceURL + "/api/session/data", this.onNewData);
|
||||
}
|
||||
|
||||
onRunningChanged = (running, deviceURL) => {
|
||||
let req = new XMLHttpRequest();
|
||||
if (running) {
|
||||
//console.log("Starting session", deviceURL + "/api/session/start");
|
||||
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;
|
||||
}
|
||||
req.addEventListener("error", evt => console.log(evt));
|
||||
req.addEventListener("abort", evt => console.log(evt));
|
||||
req.send();
|
||||
}
|
||||
|
||||
onNewData = (data) => {
|
||||
if (data.sessionStartTime == this.state.session.sessionId &&
|
||||
data.startIndex == this.state.session.rawData.size) {
|
||||
// normal case, add received data to measurement array
|
||||
const newData = this.state.session.rawData.concat(List(data.values));
|
||||
const analysis = this.analyzeNewMeasurements(data.values, this.state.session.rawData);
|
||||
this.store.dispatch(reportDeviceData(data.sessionStartTime, data.startIndex, newData, analysis));
|
||||
}
|
||||
else if (data.startIndex === 0) {
|
||||
this.resetAnalysis();
|
||||
const newData = List(data.values);
|
||||
const analysis = this.analyzeNewMeasurements(data.values, this.state.session.rawData);
|
||||
this.store.dispatch(reportDeviceData(data.sessionStartTime, data.startIndex, newData, analysis));
|
||||
} else {
|
||||
// missed some data -> re-query
|
||||
console.log("Requery :(");
|
||||
//console.log("Session times", data.sessionStartTime == this.state.session.sessionId, data.sessionStartTime, this.state.session.sessionId);
|
||||
//console.log("Index ",data.startIndex == this.state.session.rawData.size, data.startIndex, this.state.session.rawData.size);
|
||||
this.resetAnalysis();
|
||||
this.dataSource.startIndex = 0;
|
||||
this.store.dispatch(resetDeviceData());
|
||||
}
|
||||
}
|
||||
|
||||
analyzeNewMeasurements = (newData, oldData) => {
|
||||
const newPeaks = this.peakDetectorSimple.addVector(newData);
|
||||
const allPeaks = this.state.session.analysis.peaks.concat(List(newPeaks));
|
||||
|
||||
const allMeasurements = oldData.concat(List(newData));
|
||||
const totalMomentum = allMeasurements.reduce((sum, x) => sum + x, 0);
|
||||
const peakMax = allMeasurements.reduce((running, x) => Math.max(x, running), 0);
|
||||
|
||||
// windowed quantities
|
||||
const windowSizeMeasurements = WINDOW_SIZE_SECS * NUM_MEASUREMENTS_PER_SECOND;
|
||||
const windowedSeq = allMeasurements.slice(-windowSizeMeasurements);
|
||||
const peakMaxWindow = windowedSeq.reduce((running, x) => Math.max(x, running), 0);
|
||||
const momentumWindow = windowedSeq.reduce((sum, x) => sum + x, 0);
|
||||
|
||||
return {
|
||||
peaks: allPeaks,
|
||||
totalTime: allMeasurements.length / NUM_MEASUREMENTS_PER_SECOND,
|
||||
activeTime: 0,
|
||||
totalMomentum: totalMomentum,
|
||||
peakFrequency: 0,
|
||||
peakMax: peakMax,
|
||||
// windowed quantities
|
||||
momentumWindow: momentumWindow,
|
||||
frequencyWindow: 0,
|
||||
peakMaxWindow: peakMaxWindow,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default DataProcessing;
|
||||
76
SwimTracker/data_processing/DeviceDataSource.js
Normal file
76
SwimTracker/data_processing/DeviceDataSource.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import * as msgpack from 'msgpack-lite';
|
||||
|
||||
class DeviceHttpDataSource {
|
||||
|
||||
constructor(dataUrl, onNewData, pollInterval=800, startIndex = 0) {
|
||||
this.dataUrl = dataUrl;
|
||||
this.onNewData = onNewData;
|
||||
this.pollInterval = pollInterval;
|
||||
this.startIndex = startIndex;
|
||||
this.timer = null;
|
||||
|
||||
// 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;
|
||||
this.onNewData(decoded);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.timer === null) {
|
||||
console.log("Start monitoring");
|
||||
this.timer = setInterval(this.fetchDataHttp, this.pollInterval);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.timer !== null) {
|
||||
console.log("stop monitoring");
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default DeviceHttpDataSource;
|
||||
29
SwimTracker/data_processing/MovingAverage.js
Normal file
29
SwimTracker/data_processing/MovingAverage.js
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
/**
|
||||
* A moving average computation
|
||||
*/
|
||||
export class MovingAverage {
|
||||
constructor(windowSize) {
|
||||
this._windowSize = windowSize;
|
||||
this._queue = [];
|
||||
this._queueSum = 0;
|
||||
}
|
||||
|
||||
windowSize() {
|
||||
return this._windowSize;
|
||||
}
|
||||
|
||||
addVector(vec) {
|
||||
return vec.map(this.add.bind(this));
|
||||
}
|
||||
|
||||
add(value) {
|
||||
this._queueSum += value;
|
||||
this._queue.push(value);
|
||||
if(this._queue.length > this._windowSize) {
|
||||
this._queueSum -= this._queue[0];
|
||||
this._queue.shift();
|
||||
}
|
||||
return this._queueSum / this._queue.length;
|
||||
}
|
||||
};
|
||||
185
SwimTracker/data_processing/PeakDetection.js
Normal file
185
SwimTracker/data_processing/PeakDetection.js
Normal file
@@ -0,0 +1,185 @@
|
||||
|
||||
/**
|
||||
* A simple peak detector
|
||||
*
|
||||
* Usage: Successively add values via add() and query the indices of the peaks with peaks
|
||||
*
|
||||
* A peak is detected if the current point is local maximum (no filtering!) and the current
|
||||
* value is larger than (threshold + minimum_since_last_peak)
|
||||
*/
|
||||
class PeakDetectorSimple {
|
||||
constructor(threshold) {
|
||||
this._threshold = threshold;
|
||||
this._queue = [];
|
||||
this._last_min = 0;
|
||||
this._counter = 0;
|
||||
}
|
||||
|
||||
getThreshold() {
|
||||
return this._threshold;
|
||||
}
|
||||
|
||||
addVector(vec) {
|
||||
let result = [];
|
||||
|
||||
for (let i = 0; i < vec.length; ++i) {
|
||||
const res = this.add(vec[i]);
|
||||
if(res !== null)
|
||||
result.push(res);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
add(value) {
|
||||
let result = null;
|
||||
this._queue.push(value);
|
||||
if (this._queue.length > 3) {
|
||||
this._queue.shift();
|
||||
}
|
||||
if (this._queue.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
const [last, current, next] = this._queue;
|
||||
const is_maximum = current > next && current > last;
|
||||
if (is_maximum && (current - this._last_min) > this._threshold) {
|
||||
result = this._counter + 1;
|
||||
this._last_min = current;
|
||||
}
|
||||
this._last_min = Math.min(this._last_min, current);
|
||||
this._counter += 1;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of Z-Score peak detection according to
|
||||
* https://stackoverflow.com/questions/22583391/peak-signal-detection-in-realtime-timeseries-data
|
||||
*/
|
||||
class PeakDetectorZScore {
|
||||
constructor(lag, threshold, influence, handleNewPeaks) {
|
||||
this.peaks = [];
|
||||
this._filter = ZScoreFilter(lag, threshold, influence);
|
||||
this._counter = 0;
|
||||
this._previous_signal = 0;
|
||||
this._max = null;
|
||||
this._max_index = null;
|
||||
this._handleNewPeaks = handleNewPeaks;
|
||||
}
|
||||
|
||||
addVector(vec) {
|
||||
const callbackBackup = this._handleNewPeaks;
|
||||
const numPeaksBefore = this.peaks.length;
|
||||
this._handleNewPeaks = null;
|
||||
for (let i = 0; i < vec.length; ++i) {
|
||||
this.add(vec[i]);
|
||||
}
|
||||
this._handleNewPeaks = callbackBackup;
|
||||
if (numPeaksBefore != this.peaks.length) {
|
||||
this._handleNewPeaks(this.peaks);
|
||||
}
|
||||
}
|
||||
|
||||
add(value) {
|
||||
const signal = this._filter.add(value);
|
||||
if (signal != null) {
|
||||
const rising_flank = this._previous_signal !== 1 && signal === 1;
|
||||
const falling_flank = this._previous_signal === 1 && signal !== 1;
|
||||
if (rising_flank)
|
||||
this._max = -1;
|
||||
if (signal === 1 && this._max != null && value > this._max) {
|
||||
this._max = value;
|
||||
this._max_index = this._counter;
|
||||
}
|
||||
if (falling_flank) {
|
||||
this.peaks.push(this._max_index);
|
||||
if (this._handleNewPeaks) {
|
||||
this._handleNewPeaks(this.peaks);
|
||||
}
|
||||
}
|
||||
|
||||
this._previous_signal = signal;
|
||||
}
|
||||
this._counter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export { PeakDetectorSimple, PeakDetectorZScore };
|
||||
|
||||
|
||||
// --------------------------------------- Helper classes -------------------------------------------------------------
|
||||
|
||||
class StatisticsQueue {
|
||||
constructor(size) {
|
||||
this._size = size;
|
||||
|
||||
this._queue = [];
|
||||
this._queue_sum = 0; // running sum over all elements currently in _queue
|
||||
this._queue_sum_sq = 0; // sum of squared elements in _queue
|
||||
}
|
||||
|
||||
add(value) {
|
||||
this._queue.push(value);
|
||||
|
||||
this._queue_sum += value;
|
||||
this._queue_sum_sq += value * value;
|
||||
|
||||
if (this._queue.length > this._size) {
|
||||
const removed = this._queue[0];
|
||||
this._queue.shift();
|
||||
this._queue_sum -= removed;
|
||||
this._queue_sum_sq -= removed * removed;
|
||||
}
|
||||
}
|
||||
|
||||
get avg() {
|
||||
return this._queue_sum / self._queue.length;
|
||||
}
|
||||
|
||||
get variance() {
|
||||
const exp_sq = this._queue_sum_sq / self._queue.length;
|
||||
const my_avg = self.avg;
|
||||
return exp_sq - (my_avg * my_avg);
|
||||
}
|
||||
|
||||
get std_deviation() {
|
||||
return Math.sqrt(this.variance);
|
||||
}
|
||||
|
||||
get filled() {
|
||||
return this._queue.length === this._size;
|
||||
}
|
||||
}
|
||||
|
||||
class ZScoreFilter {
|
||||
|
||||
constructor(lag, threshold, influence) {
|
||||
this._threshold = threshold;
|
||||
this._influence = influence;
|
||||
|
||||
this._last_value = null;
|
||||
this._stat_queue = StatisticsQueue(lag);
|
||||
}
|
||||
|
||||
add(value) {
|
||||
let sq = this._stat_queue;
|
||||
if (!sq.filled) {
|
||||
sq.add(value);
|
||||
this._last_value = value;
|
||||
return null;
|
||||
} else {
|
||||
const avg = sq.avg;
|
||||
if (Math.abs(value - avg) > this._threshold * sq.std_deviation) {
|
||||
const signal = value > avg ? 1 : -1;
|
||||
const filtered = this._influence * value + (1 - this._influence) * this._last_value;
|
||||
sq.add(filtered);
|
||||
this._last_value = filtered;
|
||||
return signal;
|
||||
} else {
|
||||
sq.add(value);
|
||||
this._last_value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
SwimTracker/data_processing/PeakDetection.test.js
Normal file
11
SwimTracker/data_processing/PeakDetection.test.js
Normal 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]);
|
||||
});
|
||||
|
||||
});
|
||||
213
SwimTracker/data_processing/SwimTrackerWebsocketConnection.js
Normal file
213
SwimTracker/data_processing/SwimTrackerWebsocketConnection.js
Normal file
@@ -0,0 +1,213 @@
|
||||
import ReconnectingWebSocket from 'reconnecting-websocket';
|
||||
import * as msgpack from 'msgpack-lite';
|
||||
|
||||
|
||||
const OpCodes = {
|
||||
// from swim tracker device to frontend
|
||||
ERROR: 1,
|
||||
|
||||
INITIAL_INFO: 2,
|
||||
SESSION_STARTED: 3,
|
||||
SESSION_STOPPED: 4,
|
||||
SESSION_NEW_DATA: 5,
|
||||
ANSWER_USER_LIST: 6,
|
||||
ANSWER_SESSION_LIST: 7,
|
||||
WIFI_STATE_RESPONSE: 8,
|
||||
WIFI_SCAN_RESPONSE: 9,
|
||||
APP_LAYER_PING: 10,
|
||||
LOG_UPDATE: 11,
|
||||
|
||||
// from frontend to device
|
||||
START_SESSION: 128,
|
||||
STOP_SESSION: 129,
|
||||
TARE: 130,
|
||||
QUERY_USER_LIST: 131,
|
||||
QUERY_SESSION_LIST: 132,
|
||||
WIFI_STATE_SET: 133,
|
||||
WIFI_STATE_GET: 134,
|
||||
WIFI_TRIGGER_SCAN: 135,
|
||||
LOG_STREAMING_START: 136,
|
||||
LOG_STREAMING_STOP: 137
|
||||
};
|
||||
|
||||
const HEARTBEAT_TIMEOUT = 3000;
|
||||
const PROVISIONING_IP = "192.168.42.1";
|
||||
|
||||
export default class SwimTrackerWebsocketConnection {
|
||||
/**
|
||||
* Creates a new persistent websocket connection to a swimtracker device
|
||||
*
|
||||
* @param {string} swimTrackerHost hostname or ip of the swimtracker device
|
||||
* @param {(data: Uint16Array) => any} onData called whenever new measurement data is available
|
||||
* @param {(sessionId: number) => any} onStarted called when a new measurement session was started
|
||||
* @param {() => any} onStopped called when session was stopped
|
||||
* @param {(wifistate : object) => any} onWifiStateInfo wifi state contains "state" (STATION_MODE|AP_PROVISIONING|AP_SECURE) and "hostname"
|
||||
* @param {() => any} onConnect called when websocket connection was established
|
||||
* @param {() => any} onDisconnect called when websocket disconnected
|
||||
*/
|
||||
constructor(swimTrackerHost, onData, onStarted, onStopped, onWifiStateInfo, onConnect, onDisconnect, reconnectingWsOptions={}) {
|
||||
this.swimTrackerHost = swimTrackerHost;
|
||||
|
||||
this.onData = onData;
|
||||
this.onStarted = onStarted;
|
||||
this.onStopped = onStopped;
|
||||
this.onWifiStateInfo = onWifiStateInfo;
|
||||
this.onConnect = onConnect;
|
||||
this.onDisconnect = onDisconnect;
|
||||
this.onLogMessage = () => {};
|
||||
|
||||
// try configured URL and provisioning URL
|
||||
const urls = [`ws://${swimTrackerHost}:81`, `ws://${PROVISIONING_IP}:81`];
|
||||
let urlIndex = 0;
|
||||
const urlProvider = () => urls[urlIndex++ % urls.length]; // round robin url provider
|
||||
|
||||
this.ws = new ReconnectingWebSocket(urlProvider, [], { ...reconnectingWsOptions, maxReconnectionDelay: 3000 });
|
||||
this.ws.onmessage = this._onMessage;
|
||||
this.ws.onopen = this.onConnect;
|
||||
this.ws.onclose = this.onDisconnect;
|
||||
this.ws.onerror = this._onError;
|
||||
this.ws.binaryType = 'arraybuffer';
|
||||
|
||||
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._wifiScanPromises = [];
|
||||
this.pingTimeout = null;
|
||||
}
|
||||
|
||||
heartbeat() {
|
||||
clearTimeout(this.pingTimeout);
|
||||
|
||||
let connection = this;
|
||||
this.pingTimeout = setTimeout(() => {
|
||||
if(connection.ws !== null)
|
||||
connection.ws.reconnect();
|
||||
}, HEARTBEAT_TIMEOUT);
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.ws !== null) {
|
||||
this.ws.onmessage = null;
|
||||
this.ws.onopen = null;
|
||||
this.ws.onclose = null;
|
||||
this.ws.onerror = null;
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
sendStartCommand() {
|
||||
this._sendMsg(OpCodes.START_SESSION);
|
||||
}
|
||||
|
||||
sendStopCommand() {
|
||||
this._sendMsg(OpCodes.STOP_SESSION);
|
||||
}
|
||||
|
||||
sendTareCommand = () => {
|
||||
this._sendMsg(OpCodes.TARE);
|
||||
}
|
||||
|
||||
sendLogStreamStartCommand = () => {
|
||||
this._sendMsg(OpCodes.LOG_STREAMING_START);
|
||||
}
|
||||
|
||||
sendLogStreamStopCommand = () => {
|
||||
this._sendMsg(OpCodes.LOG_STREAMING_STOP);
|
||||
}
|
||||
|
||||
scanWifiNetworks() {
|
||||
console.log("Trigger wifi scan");
|
||||
this._sendMsg(OpCodes.WIFI_TRIGGER_SCAN);
|
||||
|
||||
let conn = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
conn._wifiScanPromises.push({ resolve: resolve, reject: reject });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
sendTareCommand = () => {
|
||||
this._sendMsg(OpCodes.WIFI_STATE_SET, {
|
||||
"reset_to_provisioning": true,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
wifiSetModeAP(password) {
|
||||
this._sendMsg(OpCodes.WIFI_STATE_SET, {
|
||||
"ap_password": password,
|
||||
});
|
||||
}
|
||||
|
||||
wifiSetModeSTA(ssid, password) {
|
||||
console.log("Setting sta mode", ssid, password);
|
||||
this._sendMsg(OpCodes.WIFI_STATE_SET, {
|
||||
"sta_ssid": ssid,
|
||||
"sta_password": password,
|
||||
});
|
||||
}
|
||||
|
||||
_sendMsg(code, data) {
|
||||
let msg = undefined;
|
||||
if (data) {
|
||||
const serializedData = msgpack.encode(data);
|
||||
msg = new Uint8Array([code, ...serializedData]);
|
||||
} else {
|
||||
msg = new Uint8Array(1);
|
||||
msg[0] = code;
|
||||
}
|
||||
this.ws.send(msg);
|
||||
}
|
||||
|
||||
_onMessage = (e) => {
|
||||
const dv = new DataView(e.data);
|
||||
const opCode = dv.getInt8(0);
|
||||
const payload = new Uint8Array(e.data).slice(1);
|
||||
|
||||
this.heartbeat();
|
||||
|
||||
if (opCode === OpCodes.INITIAL_INFO) {
|
||||
const headerSize = 6;
|
||||
const running = Boolean(dv.getInt8(1));
|
||||
const sessionId = dv.getUint32(2);
|
||||
if (running && e.data.byteLength > headerSize) {
|
||||
const data = new Uint16Array(e.data.slice(headerSize));
|
||||
this.onStarted(sessionId);
|
||||
this.onData(data);
|
||||
} else
|
||||
this.onStopped();
|
||||
} else if (opCode === OpCodes.SESSION_STARTED) {
|
||||
const sessionId = dv.getUint32(1);
|
||||
this.onStarted(sessionId);
|
||||
} else if (opCode === OpCodes.SESSION_STOPPED) {
|
||||
this.onStopped();
|
||||
} else if (opCode === OpCodes.SESSION_NEW_DATA) {
|
||||
const data = new Uint16Array(e.data.slice(1));
|
||||
this.onData(data);
|
||||
} else if (opCode === OpCodes.WIFI_SCAN_RESPONSE) {
|
||||
const scanResult = msgpack.decode(payload, { codec: this.msgpackCodec });
|
||||
|
||||
for (let i = 0; i < this._wifiScanPromises.length; ++i) {
|
||||
this._wifiScanPromises[i].resolve(scanResult);
|
||||
}
|
||||
this._wifiScanPromises.length = 0;
|
||||
} else if (opCode === OpCodes.WIFI_STATE_RESPONSE) {
|
||||
const wifiInfo = msgpack.decode(payload, { codec: this.msgpackCodec });
|
||||
this.onWifiStateInfo(wifiInfo);
|
||||
} else if (opCode === OpCodes.LOG_UPDATE) {
|
||||
const logMsg = msgpack.decode(payload, { codec: this.msgpackCodec });
|
||||
this.onLogMessage(logMsg);
|
||||
} else if (opCode === OpCodes.APP_LAYER_PING) {
|
||||
//console.log("got heartbeat");
|
||||
}
|
||||
}
|
||||
|
||||
_onError = (ev) => {
|
||||
console.log("Websocket error", ev);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user