Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recording Data in all three modules! #77

Merged
merged 21 commits into from
Dec 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the pop up when recording (modal window)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work getting a modal window pop'ped.


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) {
Copy link
Owner Author

@kylemath kylemath Dec 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not get the case by case pipe building working in this pull request, but I did move this to a function and have it commented out for now so we can try again in a different branch/PR

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case by case pipe building working

Is the assumption that this will significantly improve performance?
Is there an associated issue for this fix?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes assumption is it will take less memory
#79

// 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;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is what we will need to adjust depending on time desired and epoch length

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sampling rate will always be 256Hz unless it is a Muse 1 in which case it will be 220Hz.

So, time desired would be

recording_time_length = num_samples / sample_rate ... right?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it isn't the sampling rate of the EEG, it is the interval between consecutive epochs of data in neurosity pipes

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"}) + ",",
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tediously creating the headers for each csv types

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was for sure the hardest part and you have done great work making it useful for the user. Laudable efforts!

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(
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the first two we subscribe and take 1 sample to get some info for the header

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");
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

push values with commas between them to file

// 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");
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

custom names for each module

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);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now we call this function here instead of building each one

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() {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the record button and popup when it is pressed

if (selected === translations.types.intro) {
console.log('No record on intro')
return null
} else if (selected === translations.types.raw) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if recording raw data there is a suggested setting explained for the user

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>
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reformatted links for polaris

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there other links that we will need to change to conform?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not yet

</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