Comparar commits
9 Commits
| Autor | SHA1 | Data | |
|---|---|---|---|
| d819ef61ea | |||
| 4ed59b96ee | |||
| 0a0ec6eaab | |||
| a0ca1deefb | |||
| 8f72399b15 | |||
| 1c00ac0805 | |||
| 80ba445186 | |||
| c3e9925b2d | |||
| 5ae8742861 |
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { MuseClient } from "muse-js";
|
||||
import { Select, Card, Stack, Button, ButtonGroup } from "@shopify/polaris";
|
||||
|
||||
@@ -15,9 +15,10 @@ import * as funSpectra from "./components/EEGEduSpectra/EEGEduSpectra";
|
||||
import * as funBands from "./components/EEGEduBands/EEGEduBands";
|
||||
import * as funAnimate from "./components/EEGEduAnimate/EEGEduAnimate";
|
||||
import * as funSpectro from "./components/EEGEduSpectro/EEGEduSpectro";
|
||||
import * as funAlpha from "./components/EEGEduAlpha/EEGEduAlpha"
|
||||
import * as funSsvep from "./components/EEGEduSsvep/EEGEduSsvep"
|
||||
import * as funPredict from "./components/EEGEduPredict/EEGEduPredict"
|
||||
import * as funAlpha from "./components/EEGEduAlpha/EEGEduAlpha";
|
||||
import * as funSsvep from "./components/EEGEduSsvep/EEGEduSsvep";
|
||||
import * as funEvoked from "./components/EEGEduEvoked/EEGEduEvoked";
|
||||
import * as funPredict from "./components/EEGEduPredict/EEGEduPredict";
|
||||
|
||||
const intro = translations.types.intro;
|
||||
const heartRaw = translations.types.heartRaw;
|
||||
@@ -29,6 +30,7 @@ const animate = translations.types.animate;
|
||||
const spectro = translations.types.spectro;
|
||||
const alpha = translations.types.alpha;
|
||||
const ssvep = translations.types.ssvep;
|
||||
const evoked = translations.types.evoked;
|
||||
const predict = translations.types.predict;
|
||||
|
||||
export function PageSwitcher() {
|
||||
@@ -44,6 +46,7 @@ export function PageSwitcher() {
|
||||
const [spectroData, setSpectroData] = useState(emptyChannelData);
|
||||
const [alphaData, setAlphaData] = useState(emptyChannelData);
|
||||
const [ssvepData, setSsvepData] = useState(emptyChannelData);
|
||||
const [evokedData, setEvokedData] = useState(emptyChannelData);
|
||||
const [predictData, setPredictData] = useState(emptyChannelData);
|
||||
|
||||
// pipe settings
|
||||
@@ -57,6 +60,7 @@ export function PageSwitcher() {
|
||||
const [spectroSettings, setSpectroSettings] = useState(funSpectro.getSettings);
|
||||
const [alphaSettings, setAlphaSettings] = useState(funAlpha.getSettings);
|
||||
const [ssvepSettings, setSsvepSettings] = useState(funSsvep.getSettings);
|
||||
const [evokedSettings] = useState(funEvoked.getSettings);
|
||||
const [predictSettings, setPredictSettings] = useState(funPredict.getSettings);
|
||||
|
||||
// connection status
|
||||
@@ -79,6 +83,7 @@ export function PageSwitcher() {
|
||||
if (window.subscriptionSpectro) window.subscriptionSpectro.unsubscribe();
|
||||
if (window.subscriptionAlpha) window.subscriptionAlpha.unsubscribe();
|
||||
if (window.subscriptionSsvep) window.subscriptionSsvep.unsubscribe();
|
||||
if (window.subscriptionEvoked) window.subscriptionEvoked.unsubscribe();
|
||||
if (window.subscriptionPredict) window.subscriptionPredict.unsubscribe();
|
||||
|
||||
subscriptionSetup(value);
|
||||
@@ -104,6 +109,7 @@ export function PageSwitcher() {
|
||||
{ label: spectro, value: spectro },
|
||||
{ label: alpha, value: alpha },
|
||||
{ label: ssvep, value: ssvep },
|
||||
{ label: evoked, value: evoked },
|
||||
{ label: predict, value: predict }
|
||||
|
||||
];
|
||||
@@ -119,6 +125,7 @@ export function PageSwitcher() {
|
||||
funSpectro.buildPipe(spectroSettings);
|
||||
funAlpha.buildPipe(alphaSettings);
|
||||
funSsvep.buildPipe(ssvepSettings);
|
||||
funEvoked.buildPipe(evokedSettings);
|
||||
funPredict.buildPipe(predictSettings);
|
||||
}
|
||||
|
||||
@@ -154,6 +161,9 @@ export function PageSwitcher() {
|
||||
case ssvep:
|
||||
funSsvep.setup(setSsvepData, ssvepSettings);
|
||||
break;
|
||||
case evoked:
|
||||
funEvoked.setup(setEvokedData, evokedSettings);
|
||||
break;
|
||||
case predict:
|
||||
funPredict.setup(setPredictData, predictSettings);
|
||||
break;
|
||||
@@ -236,6 +246,8 @@ export function PageSwitcher() {
|
||||
return (
|
||||
funSsvep.renderSliders(setSsvepData, setSsvepSettings, status, ssvepSettings)
|
||||
);
|
||||
case evoked:
|
||||
return null
|
||||
case predict:
|
||||
return (
|
||||
funPredict.renderSliders(setPredictData, setPredictSettings, status, predictSettings)
|
||||
@@ -266,6 +278,8 @@ export function PageSwitcher() {
|
||||
return <funAlpha.renderModule data={alphaData} />;
|
||||
case ssvep:
|
||||
return <funSsvep.renderModule data={ssvepData} />;
|
||||
case evoked:
|
||||
return <funEvoked.renderModule data={evokedData} />;
|
||||
case predict:
|
||||
return <funPredict.renderModule data={predictData} />;
|
||||
default:
|
||||
@@ -307,6 +321,10 @@ export function PageSwitcher() {
|
||||
return (
|
||||
funSsvep.renderRecord(recordPopChange, recordPop, status, ssvepSettings, recordTwoPopChange, recordTwoPop)
|
||||
)
|
||||
case evoked:
|
||||
return (
|
||||
funEvoked.renderRecord(recordPopChange, recordPop, status, evokedSettings)
|
||||
)
|
||||
case predict:
|
||||
return (
|
||||
funPredict.renderRecord(status)
|
||||
|
||||
@@ -36,7 +36,8 @@ export function getSettings () {
|
||||
interval: 16,
|
||||
bins: 256,
|
||||
duration: 128,
|
||||
srate: 256
|
||||
srate: 256,
|
||||
name: 'Animate'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
import React from "react";
|
||||
import { catchError, multicast } from "rxjs/operators";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
|
||||
import { saveAs } from 'file-saver';
|
||||
import { take } from "rxjs/operators";
|
||||
|
||||
|
||||
import { zipSamples } from "muse-js";
|
||||
|
||||
import {
|
||||
bandpassFilter,
|
||||
epoch
|
||||
} from "@neurosity/pipes";
|
||||
|
||||
import { chartStyles } from "../chartOptions";
|
||||
|
||||
import * as generalTranslations from "../translations/en";
|
||||
import * as specificTranslations from "./translations/en";
|
||||
|
||||
import { generateXTics, standardDeviation } from "../../utils/chartUtils";
|
||||
|
||||
import P5Wrapper from 'react-p5-wrapper';
|
||||
import sketchEvoked from './sketchEvoked'
|
||||
|
||||
export function getSettings () {
|
||||
return {
|
||||
cutOffLow: 2,
|
||||
cutOffHigh: 20,
|
||||
nbChannels: 4,
|
||||
interval: 1,
|
||||
srate: 256,
|
||||
duration: 1,
|
||||
name: 'Evoked'
|
||||
}
|
||||
};
|
||||
|
||||
export function buildPipe(Settings) {
|
||||
if (window.subscriptionEvoked) window.subscriptionEvoked.unsubscribe();
|
||||
|
||||
window.pipeEvoked$ = null;
|
||||
window.multicastEvoked$ = null;
|
||||
window.subscriptionEvoked = null;
|
||||
|
||||
// Build Pipe
|
||||
window.pipeEvoked$ = zipSamples(window.source.eegReadings$).pipe(
|
||||
bandpassFilter({
|
||||
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
|
||||
nbChannels: Settings.nbChannels }),
|
||||
epoch({
|
||||
duration: Settings.duration,
|
||||
interval: Settings.interval,
|
||||
samplingRate: Settings.srate
|
||||
}),
|
||||
catchError(err => {
|
||||
console.log(err);
|
||||
})
|
||||
);
|
||||
window.multicastEvoked$ = window.pipeEvoked$.pipe(
|
||||
multicast(() => new Subject())
|
||||
);
|
||||
}
|
||||
|
||||
export function setup(setData, Settings) {
|
||||
console.log("Subscribing to " + Settings.name);
|
||||
|
||||
if (window.multicastEvoked$) {
|
||||
window.subscriptionEvoked = window.multicastEvoked$.subscribe(data => {
|
||||
setData(evokedData => {
|
||||
Object.values(evokedData).forEach((channel, index) => {
|
||||
if (index < 4) {
|
||||
channel.datasets[0].data = data.data[index];
|
||||
channel.xLabels = generateXTics(Settings.srate, Settings.duration);
|
||||
channel.datasets[0].qual = standardDeviation(data.data[index])
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
ch0: evokedData.ch0,
|
||||
ch1: evokedData.ch1,
|
||||
ch2: evokedData.ch2,
|
||||
ch3: evokedData.ch3
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
window.multicastEvoked$.connect();
|
||||
console.log("Subscribed to Evoked");
|
||||
}
|
||||
}
|
||||
|
||||
export function renderModule(channels) {
|
||||
function renderCharts() {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={specificTranslations.title}>
|
||||
<Card.Section>
|
||||
<Stack>
|
||||
<TextContainer>
|
||||
<p>{specificTranslations.description}</p>
|
||||
</TextContainer>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
<Card.Section>
|
||||
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderSliders(setData, setSettings, status, Settings) {
|
||||
|
||||
function resetPipeSetup(value) {
|
||||
buildPipe(Settings);
|
||||
setup(setData, Settings)
|
||||
}
|
||||
|
||||
function handleIntervalRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, interval: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleCutoffLowRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, cutOffLow: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleCutoffHighRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, cutOffHigh: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleDurationRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, duration: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={Settings.name + ' Settings'} sectioned>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={128} step={128} max={4096}
|
||||
label={'Epoch duration (Sampling Points): ' + Settings.duration}
|
||||
value={Settings.duration}
|
||||
onChange={handleDurationRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={10} step={1} max={Settings.duration}
|
||||
label={'Sampling points between epochs onsets: ' + Settings.interval}
|
||||
value={Settings.interval}
|
||||
onChange={handleIntervalRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={.01} step={.5} max={Settings.cutOffHigh - .5}
|
||||
label={'Cutoff Frequency Low: ' + Settings.cutOffLow + ' Hz'}
|
||||
value={Settings.cutOffLow}
|
||||
onChange={handleCutoffLowRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={Settings.cutOffLow + .5} step={.5} max={Settings.srate/2}
|
||||
label={'Cutoff Frequency High: ' + Settings.cutOffHigh + ' Hz'}
|
||||
value={Settings.cutOffHigh}
|
||||
onChange={handleCutoffHighRangeSliderChange}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderRecord(recordPopChange, recordPop, status, Settings) {
|
||||
return (
|
||||
<Card title={'Run ERP experiment'} sectioned>
|
||||
<Card.Section>
|
||||
<p>
|
||||
{"Clicking this button will begin the experiment so check your data quality on the raw module first. "}
|
||||
{"A window will pop up when you click the button and a series of circles will appear. Stare at the cross in the center. "}
|
||||
{"There will be red and blue circles, ignore the blue ones."}
|
||||
{"Whenever you see a red circle, as fast as you can either press spacebar on a keyboard, or tap the touchscreen on a tablet or phone. "}
|
||||
{"This entire time the eeg data will be saved along with a column indicating which target was on the screen, and another for the responses. "}
|
||||
{"The task will continue for a few minutes and once it is finished a .csv file will automatically download. "}
|
||||
{"This .csv file has a row for each time point, a column for each electrode, and the columns indicating when targets appeared, and when responses were made. "}
|
||||
{"It saves a marker of 20 when the target is on, a marker of 10 when the blue standards are on, and a peak when the spacebar or touchscreen are pressed, here you can see those synced with an eeg channel: "}
|
||||
|
||||
</p>
|
||||
<p>
|
||||
<img
|
||||
src={ require("./dataExample.png")}
|
||||
alt="dataExample"
|
||||
width="100%"
|
||||
height="auto"
|
||||
></img>
|
||||
</p>
|
||||
</Card.Section>
|
||||
<Stack>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveToCSV(Settings);
|
||||
recordPopChange();
|
||||
}}
|
||||
primary={status !== generalTranslations.connect}
|
||||
disabled={status === generalTranslations.connect}
|
||||
>
|
||||
{'Run oddball experiment'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<Modal
|
||||
open={recordPop}
|
||||
onClose={recordPopChange}
|
||||
title={"Press Spacebar or tap screen when you see RED circle"}
|
||||
>
|
||||
<Modal.Section>
|
||||
<Card.Section>
|
||||
<P5Wrapper sketch={sketchEvoked} />
|
||||
</Card.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>
|
||||
)
|
||||
}
|
||||
|
||||
function saveToCSV(Settings) {
|
||||
const numSamplesToSave = 10000;
|
||||
console.log('Saving ' + numSamplesToSave + ' samples...');
|
||||
var localObservable$ = null;
|
||||
const dataToSave = [];
|
||||
window.marker = 0;
|
||||
window.responseMarker = 0;
|
||||
window.touchMarker = 0;
|
||||
|
||||
console.log('making ' + Settings.name + ' headers')
|
||||
|
||||
// for each module subscribe to multicast and make header
|
||||
// take one sample from selected observable object for headers
|
||||
localObservable$ = window.multicastEvoked$.pipe(
|
||||
take(1)
|
||||
);
|
||||
//take one sample to get header info
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
dataToSave.push(
|
||||
"Timestamp (ms),",
|
||||
"Marker,",
|
||||
"SpaceBar,",
|
||||
"TouchMarker,",
|
||||
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"
|
||||
);
|
||||
}
|
||||
});
|
||||
// put selected observable object into local and start taking samples
|
||||
localObservable$ = window.multicastEvoked$.pipe(
|
||||
take(numSamplesToSave)
|
||||
);
|
||||
|
||||
// now with header in place subscribe to each epoch and log it
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
dataToSave.push(
|
||||
Date.now() + "," +
|
||||
window.marker + "," +
|
||||
window.responseMarker + "," +
|
||||
window.touchMarker + "," +
|
||||
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, Settings.name + "_Recording_" + Date.now() + ".csv");
|
||||
console.log('Completed');
|
||||
}
|
||||
});
|
||||
}
|
||||
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 224 KiB |
@@ -0,0 +1,64 @@
|
||||
export default function sketchEvoked (p) {
|
||||
|
||||
let x = 0;
|
||||
let thisRand = 0.5; //for random choice of target type
|
||||
let targProp = 0.25;
|
||||
let isTarget = false;
|
||||
|
||||
|
||||
p.setup = function () {
|
||||
p.createCanvas(300, 300);
|
||||
p.frameRate(30);
|
||||
p.noStroke();
|
||||
|
||||
};
|
||||
|
||||
p.windowResized = function() {
|
||||
p.createCanvas(300, 300);
|
||||
}
|
||||
|
||||
p.touchStarted = function() {
|
||||
if (window.touchMarker === 0) {
|
||||
window.touchMarker = 255;
|
||||
} else {
|
||||
window.touchMarker = 0;
|
||||
}
|
||||
}
|
||||
|
||||
p.draw = function () {
|
||||
|
||||
if (p.keyIsPressed === true) {
|
||||
window.responseMarker = p.keyCode;
|
||||
} else {
|
||||
window.responseMarker = 0;
|
||||
}
|
||||
p.background(255);
|
||||
x = x+1;
|
||||
// var num = int(random(0, 11));
|
||||
if (x % 20 === 0) { // When a target shown (every ith frame for now)
|
||||
|
||||
thisRand = p.random();
|
||||
if (thisRand < targProp) { // targets 20% of the time
|
||||
isTarget = true;
|
||||
} else {
|
||||
isTarget = false;
|
||||
}
|
||||
|
||||
if (isTarget) {
|
||||
p.fill(250, 150, 150);
|
||||
window.marker = 20;
|
||||
} else {
|
||||
p.fill(150,150,250);
|
||||
window.marker = 10;
|
||||
}
|
||||
|
||||
} else { // during time between targets
|
||||
p.fill(255, 255, 255);
|
||||
window.marker = 0;
|
||||
}
|
||||
p.ellipse(p.width/2, p.height/2, 300);
|
||||
p.fill(255,0,0);
|
||||
p.text("+", p.width/2, p.height/2);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"title": "Stimulus Evoked Event-related potential (ERP)",
|
||||
"description": [
|
||||
"The electrical activity evoked by individual presentations of stimuli does create small changes in electrical voltage. ",
|
||||
"These changes are, however, very small, about 1-10 microVolts (\u03BCV), and the noise from other things like eye movements and muscles is sometimes 10x as large. ",
|
||||
"This noise can be mitigated by averaging this evoked activity over repeated presentations of the same stimulus. ",
|
||||
"This average voltage in response to a stimulus is called an Event-related potential (ERP) or evoked potential (EP). ",
|
||||
"Here..."
|
||||
],
|
||||
"xlabel": "Time (ms)",
|
||||
"ylabel": "Votage (\u03BCV)"
|
||||
}
|
||||
@@ -8,6 +8,6 @@
|
||||
"Along the horizontal axis is the beats per minute, or the frequency of the peaks in your ECG. ",
|
||||
"The vertical y-axis shows the power of the rhythms in the data at each frequency, or how large the changes are between peak and through of the oscillations. "
|
||||
],
|
||||
"xlabel": "Frequency (Hz)",
|
||||
"xlabel": "Heart Frequency (BPM)",
|
||||
"ylabel": "Power (\u03BCV\u00B2)"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export default function sketchFlash (p) {
|
||||
|
||||
p.setup = function () {
|
||||
p.createCanvas(300, 300);
|
||||
p.frameRate(30);
|
||||
};
|
||||
|
||||
p.windowResized = function() {
|
||||
|
||||
@@ -4,6 +4,7 @@ export default function sketchFlash (p) {
|
||||
|
||||
p.setup = function () {
|
||||
p.createCanvas(300, 300);
|
||||
p.frameRate(30);
|
||||
};
|
||||
|
||||
p.windowResized = function() {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"spectro": "8. Spectrogram (spectra over time)",
|
||||
"alpha": "9. Eyes open vs. Eyes closed Experiment",
|
||||
"ssvep": "10. Steady-State Visual Evoked Potential (SSVEP) Experiment",
|
||||
"predict": "11. Predict brain states with a trained classifier"
|
||||
"evoked": "11. Stimulus Evoked Event-related potential (ERP)",
|
||||
"predict": "12. Predict brain states with a trained classifier"
|
||||
}
|
||||
}
|
||||
|
||||
Referência em uma Nova Issue
Bloquear um usuário