Skip to content

Commit

Permalink
Merge pull request #77 from kylemath/streamsaver2
Browse files Browse the repository at this point in the history
Recording Data in all three modules!
  • Loading branch information
kylemath authored Dec 24, 2019
2 parents 8208706 + 6968d05 commit 1c0db33
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 28 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@neurosity/pipes": "^3.0.2",
"@shopify/polaris": "^4.9.0",
"chart.js": "^2.7.2",
"file-saver": "^2.0.2",
"firebase": "^7.5.0",
"firebase-tools": "^7.9.0",
"muse-js": "^3.0.1",
Expand Down
234 changes: 223 additions & 11 deletions src/components/PageSwitcher/PageSwitcher.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useState, useCallback } from "react";

import { Select, Card, Stack, Button, ButtonGroup } from "@shopify/polaris";
import { Select, Card, Stack, Button, ButtonGroup, Modal, TextContainer } from "@shopify/polaris";

import { mockMuseEEG } from "./utils/mockMuseEEG";

import { generateXTics } from "./utils/chartUtils";

import * as Intro from "./components/EEGEduIntro/EEGEduIntro"
import * as Raw from "./components/EEGEduRaw/EEGEduRaw";
import * as Spectra from "./components/EEGEduSpectra/EEGEduSpectra";
Expand All @@ -13,20 +15,26 @@ import * as translations from "./translations/en.json";
import { MuseClient } from "muse-js";
import * as generalTranslations from "./components/translations/en";
import { emptyChannelData } from "./components/chartOptions";
import { saveAs } from 'file-saver';
import { take } from "rxjs/operators";

export function PageSwitcher() {

const [introData, setIntroData] = useState(emptyChannelData)
const [rawData, setRawData] = useState(emptyChannelData);
const [spectraData, setSpectraData] = useState(emptyChannelData);
const [bandsData, setBandsData] = useState(emptyChannelData);
const [recordPop, setRecordPop] = useState(false);

const [introSettings] = useState(Intro.getSettings);
const [spectraSettings, setSpectraSettings] = useState(Spectra.getSettings);
const [rawSettings, setRawSettings] = useState(Raw.getSettings);
const [bandsSettings, setBandsSettings] = useState(Bands.getSettings);

const [status, setStatus] = useState(generalTranslations.connect);

const recordPopChange = useCallback(() => setRecordPop(!recordPop), [recordPop]);

// module at load:
const [selected, setSelected] = useState(translations.types.intro);
const handleSelectChange = useCallback(value => {
Expand All @@ -40,6 +48,9 @@ export function PageSwitcher() {
if (window.subscriptionSpectra$) window.subscriptionSpectra$.unsubscribe();
if (window.subscriptionBands$) window.subscriptionBands$.unsubscribe();

// buildPipe(value);
// if the case statements are uncommented below,
// need this, but then this crashes since runs before source is ready
subscriptionSetup(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand All @@ -51,6 +62,24 @@ export function PageSwitcher() {
{ label: translations.types.bands, value: translations.types.bands }
];

function buildPipe(value) {
// switch (value) {
// case translations.types.intro:
Intro.buildPipe(introSettings);
// break;
// case translations.types.raw:
Raw.buildPipe(rawSettings);
// break;
// case translations.types.spectra:
Spectra.buildPipe(spectraSettings);
// break;
// case translations.types.bands:
Bands.buildPipe(bandsSettings);
// break;
// default: console.log('Error building pipe')
// }
}

function subscriptionSetup(value) {
switch (value) {
case translations.types.intro:
Expand All @@ -72,6 +101,106 @@ export function PageSwitcher() {
}
}

function saveToCSV(value) {
const numSamplesToSave = 50;
console.log('Saving ' + numSamplesToSave + ' samples...');
var localObservable$ = null;
const dataToSave = [];

// for each module subscribe to multicast and make header
switch (value) {
case translations.types.raw:
//take one sample to get header info
localObservable$ = window.multicastRaw$.pipe(
take(1)
);
localObservable$.subscribe({
next(x) {
dataToSave.push(
"Timestamp (ms),",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch0_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch1_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch2_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch3_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "chAux_" + f + "ms"}) + ",",
"info",
"\n"
);
}
});
console.log('making spectra headers')


localObservable$ = window.multicastRaw$.pipe(
take(numSamplesToSave)
);
break;
case translations.types.spectra:
//take one sample to get header info
localObservable$ = window.multicastSpectra$.pipe(
take(1)
);
localObservable$.subscribe({
next(x) {
let freqs = Object.values(x.freqs);
dataToSave.push(
"Timestamp (ms),",
freqs.map(function(f) {return "ch0_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch1_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch2_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch3_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "chAux_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "f_" + f + "Hz"}) + "," ,
"info",
"\n"
);
}
});
console.log('making spectra headers')

localObservable$ = window.multicastSpectra$.pipe(
take(numSamplesToSave)
);
break;
case translations.types.bands:
console.log('making bands headers')
dataToSave.push(
"Timestamp (ms),",
"delta0,delta1,delta2,delta3,deltaAux,",
"theta0,theta1,theta2,theta3,thetaAux,",
"alpha0,alpha1,alpha2,alpha3,alphaAux,",
"beta0,beta1,beta2,beta3,betaAux,",
"delta0,delta1,delta2,delta3,deltaAux\n"
);
localObservable$ = window.multicastBands$.pipe(
take(numSamplesToSave)
);
break;
default:
console.log(
"Error on save to CSV: " + value
);
}

localObservable$.subscribe({
next(x) {
dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n");
// logging is useful for debugging -yup
// console.log(x);
},
error(err) { console.log(err); },
complete() {
console.log('Trying to save')
var blob = new Blob(
dataToSave,
{type: "text/plain;charset=utf-8"}
);
saveAs(blob, value + "_Recording.csv");
console.log('Completed');
}
});
}

async function connect() {
try {
if (window.debugWithMock) {
Expand Down Expand Up @@ -101,17 +230,14 @@ export function PageSwitcher() {
window.source$.eegReadings
) {
console.log("Connected to data source observable");
console.log("Starting to build the data pipes from the data source...");

Intro.buildPipe(introSettings);
Raw.buildPipe(rawSettings);
Spectra.buildPipe(spectraSettings);
Bands.buildPipe(bandsSettings);

// Build the data source from the data source
// Build the data source
console.log("Starting to build the data pipes from the data source...");
buildPipe(selected);
console.log("Finished building the data pipes from the data source");

subscriptionSetup(selected);
console.log("Finished subscribing to the data source");

}
} catch (err) {
setStatus(generalTranslations.connect);
Expand Down Expand Up @@ -168,7 +294,92 @@ export function PageSwitcher() {
}
}

return (
function renderRecord() {
if (selected === translations.types.intro) {
console.log('No record on intro')
return null
} else if (selected === translations.types.raw) {
return (
<Card title={'Record Raw Data'} sectioned>
<Card.Section>
<p>
{"If you are recording raw data it is recommended you set the "}
{"number of sampling points between epochs onsets to be equal to the epoch duration. "}
{"This will ensure that consecutive rows of your output file are not overlapping in time."}
{"It will make the live plots appear more choppy."}
</p>
</Card.Section>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(selected);
recordPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Save to CSV'}
</Button>
</ButtonGroup>
<Modal
open={recordPop}
onClose={recordPopChange}
title="Recording Data"
>
<Modal.Section>
<TextContainer>
<p>
Your data is currently recording,
once complete it will be downloaded as a .csv file
and can be opened with your favorite spreadsheet program.
Close this window once the download completes.
</p>
</TextContainer>
</Modal.Section>
</Modal>
</Stack>
</Card>
)
} else {
return (
<Card title={'Record Data'} sectioned>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(selected);
recordPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Save to CSV'}
</Button>
</ButtonGroup>
<Modal
open={recordPop}
onClose={recordPopChange}
title="Recording Data"
>
<Modal.Section>
<TextContainer>
<p>
Your data is currently recording,
once complete it will be downloaded as a .csv file
and can be opened with your favorite spreadsheet program.
Close this window once the download completes.
</p>
</TextContainer>
</Modal.Section>
</Modal>
</Stack>
</Card>
)
}
}

return (
<React.Fragment>
<Card sectioned>
<Stack>
Expand Down Expand Up @@ -199,7 +410,7 @@ export function PageSwitcher() {
disabled={status === generalTranslations.connect}
>
{generalTranslations.disconnect}
</Button>
</Button>
</ButtonGroup>
</Stack>
</Card>
Expand All @@ -213,6 +424,7 @@ export function PageSwitcher() {
</Card>
{pipeSettingsDisplay()}
{renderCharts()}
{renderRecord()}
</React.Fragment>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export function buildPipe(Settings) {
window.multicastBands$ = null;
window.subscriptionBands$ = null;

// const takeSingle = window.source$.eegReadings.pipe(take(3));
// takeSingle.subscribe((v) => {console.log(v);})

window.pipeBands$ = zipSamples(window.source$.eegReadings).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { Subject } from "rxjs";

import { Card } from "@shopify/polaris";
import { Card, Link } from "@shopify/polaris";

import { Line } from "react-chartjs-2";

Expand Down Expand Up @@ -299,23 +299,23 @@ export function EEGEdu(channels) {
<Card.Section>
<p>
{specificTranslations.credits1}
<a href="http://learn.neurotechedu.com/">NeurotechEdu. </a>
<Link url="http://learn.neurotechedu.com/">NeurotechEdu. </Link>
</p>
<p>
{specificTranslations.credits2}
<a href="https://choosemuse.com/muse-research/">Interaxon. </a>
<Link url="https://choosemuse.com/muse-research/">Interaxon. </Link>
</p>
<p>
{specificTranslations.credits3}
<a href="https://github.com/urish/muse-js">muse-js </a>
<Link url="https://github.com/urish/muse-js">muse-js </Link>
{specificTranslations.credits4}
<a href="https://medium.com/neurotechx/a-techys-introduction-to-neuroscience-3f492df4d3bf">A Techy's Introduction to Neuroscience. </a>
<Link url="https://medium.com/neurotechx/a-techys-introduction-to-neuroscience-3f492df4d3bf">A Techy's Introduction to Neuroscience. </Link>
</p>
<p>
{specificTranslations.credits5}
<a href="https://github.com/neurosity/eeg-pipes">eeg-pipes </a>
<Link url="https://github.com/neurosity/eeg-pipes">eeg-pipes </Link>
{specificTranslations.credits6}
<a href="https://medium.com/@castillo.io/muse-2016-headband-web-bluetooth-11ddcfa74c83">Muse 2016 Headband + Web Bluetooth.</a>
<Link url="https://medium.com/@castillo.io/muse-2016-headband-web-bluetooth-11ddcfa74c83">Muse 2016 Headband + Web Bluetooth.</Link>
</p>
</Card.Section>
</Card>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export function renderSliders(setData, setSettings, status, Settings) {
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={10} step={5} max={Settings.duration}
min={10} step={1} max={Settings.duration}
label={'Sampling points between epochs onsets: ' + Settings.interval}
value={Settings.interval}
onChange={handleIntervalRangeSliderChange}
Expand Down
Loading

0 comments on commit 1c0db33

Please sign in to comment.