-
Notifications
You must be signed in to change notification settings - Fork 39
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
Changes from all commits
85e40b8
cc35f17
ee769be
46b0c13
cf98f6a
53b2433
ada713b
7ecdaa8
53db159
e37294a
cf2ee2f
64f3633
41877c4
3bb53bd
ae12387
a08a181
e88dcf3
d164bbd
e32ed38
d677732
6968d05
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"; | ||
|
@@ -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 => { | ||
|
@@ -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 | ||
}, []); | ||
|
@@ -51,6 +62,24 @@ export function PageSwitcher() { | |
{ label: translations.types.bands, value: translations.types.bands } | ||
]; | ||
|
||
function buildPipe(value) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Is the assumption that this will significantly improve performance? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes assumption is it will take less memory |
||
// 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: | ||
|
@@ -72,6 +101,106 @@ export function PageSwitcher() { | |
} | ||
} | ||
|
||
function saveToCSV(value) { | ||
const numSamplesToSave = 50; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"}) + ",", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tediously creating the headers for each csv types There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
@@ -168,7 +294,92 @@ export function PageSwitcher() { | |
} | ||
} | ||
|
||
return ( | ||
function renderRecord() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
@@ -199,7 +410,7 @@ export function PageSwitcher() { | |
disabled={status === generalTranslations.connect} | ||
> | ||
{generalTranslations.disconnect} | ||
</Button> | ||
</Button> | ||
</ButtonGroup> | ||
</Stack> | ||
</Card> | ||
|
@@ -213,6 +424,7 @@ export function PageSwitcher() { | |
</Card> | ||
{pipeSettingsDisplay()} | ||
{renderCharts()} | ||
{renderRecord()} | ||
</React.Fragment> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"; | ||
|
||
|
@@ -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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reformatted links for polaris There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are there other links that we will need to change to conform? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.