An easy-to-use platform for EEG experimentation in the classroom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Updated Home UI; Reduced default epoch number in editor; Added some more error handling to device epics; Changed params to run of trial number instead of time; Added explorer popup icon"

jdpigeon de6b1485 a5a1708e

+66 -53
app/assets/common/brainwaves_logo.png

This is a binary file and will not be displayed.

app/assets/common/brainwaves_logo_nih.png

This is a binary file and will not be displayed.

app/assets/common/fixationcross.png

This is a binary file and will not be displayed.

app/assets/stroop/match_b.png

This is a binary file and will not be displayed.

app/assets/stroop/match_g.png

This is a binary file and will not be displayed.

app/assets/stroop/match_p.png

This is a binary file and will not be displayed.

app/assets/stroop/match_r.png

This is a binary file and will not be displayed.

app/assets/stroop/mismatch10_r.png

This is a binary file and will not be displayed.

app/assets/stroop/mismatch11_g.png

This is a binary file and will not be displayed.

app/assets/stroop/mismatch12_b.png

This is a binary file and will not be displayed.

app/assets/stroop/mismatch1_b.png

This is a binary file and will not be displayed.

app/assets/stroop/mismatch2_g.png

This is a binary file and will not be displayed.

app/assets/stroop/mismatch3_p.png

This is a binary file and will not be displayed.

app/assets/stroop/mismatch4_b.png

This is a binary file and will not be displayed.

app/assets/stroop/mismatch5_p.png

This is a binary file and will not be displayed.

app/assets/stroop/mismatch6_r.png

This is a binary file and will not be displayed.

app/assets/stroop/mismatch7_p.png

This is a binary file and will not be displayed.

app/assets/stroop/mismatch8_r.png

This is a binary file and will not be displayed.

app/assets/stroop/mismatch9_g.png

This is a binary file and will not be displayed.

+22 -14
app/components/HomeComponent/index.js
··· 5 5 import styles from '../styles/common.css'; 6 6 import { EXPERIMENTS, SCREENS, KERNEL_STATUS } from '../../constants/constants'; 7 7 import faceHouseIcon from '../../assets/face_house/face_house_icon.jpg'; 8 - import brainwavesLogo from '../../assets/common/brainwaves_logo_nih.png'; 8 + import brainwavesLogo from '../../assets/common/brainwaves_logo.png'; 9 9 import { 10 10 readWorkspaces, 11 - readAndParseState 11 + readAndParseState, 12 + openWorkspaceDir 12 13 } from '../../utils/filesystem/storage'; 13 14 import InputModal from '../InputModal'; 14 15 import SecondaryNavComponent from '../SecondaryNavComponent'; ··· 123 124 secondary 124 125 onClick={() => this.handleLoadRecentWorkspace(dir)} 125 126 > 126 - Open Workspace 127 + Open Experiment 127 128 </Button> 128 - <Segment basic compact textAlign="center"> 129 - <Header as="h3"> {dir}</Header> 129 + <Segment className={styles.recentDirSegment} vertical basic> 130 + <Header as="h3">{dir}</Header> 130 131 </Segment> 132 + <Button 133 + icon="folder open outline" 134 + basic 135 + circular 136 + size="huge" 137 + className={styles.closeButton} 138 + onClick={() => openWorkspaceDir(dir)} 139 + /> 131 140 </Grid.Row> 132 141 ))} 133 142 </Grid> ··· 163 172 <Grid.Column> 164 173 <Segment basic className={styles.descriptionContainer}> 165 174 <Image size="huge" src={faceHouseIcon} /> 166 - <Header as="h1">Oddball</Header> 175 + <Header as="h1">Stroop</Header> 167 176 <p> 168 - Explore the P300 ERP that is produced after an unexpected 169 - oddball stimulus. The P300 ERP is a positive deflection that 170 - occurs 300ms after stimulus onset. 177 + Investigate the cognitive process of selective attention with 178 + the Stroop Task, a challenging experiment requiring the 179 + subject to name the color of a word instead of reading the 180 + word itself. 171 181 </p> 172 - <Button secondary disabled> 182 + <Button disabled secondary> 173 183 Review 174 184 </Button> 175 185 <Button 176 186 disabled 177 187 primary 178 - onClick={() => this.handleNewExperiment(EXPERIMENTS.P300)} 188 + onClick={() => this.handleNewExperiment(EXPERIMENTS.STROOP)} 179 189 > 180 190 Start Experiment 181 191 </Button> ··· 215 225 return ( 216 226 <React.Fragment> 217 227 <SecondaryNavComponent 218 - title={ 219 - <Image className={styles.brainwavesLogo} src={brainwavesLogo} /> 220 - } 228 + title={<Image src={brainwavesLogo} />} 221 229 steps={HOME_STEPS} 222 230 activeStep={this.state.activeStep} 223 231 onStepClick={this.handleStepClick}
+4
app/components/styles/common.css
··· 75 75 .paramSlider { 76 76 width: 200px; 77 77 } 78 + 79 + .recentDirSegment { 80 + margin-left: 20px !important; 81 + }
+6 -6
app/constants/constants.js
··· 1 1 export const EXPERIMENTS = { 2 2 NONE: 'NONE', 3 - P300: 'P300 Experiment', 4 - N170: 'Faces Houses Experiment', 5 - SSVEP: 'SSVEP Experiment', 6 - STROOP: 'Stroop Task Experiment', 7 - CUSTOM: 'Custom Experiment' 3 + P300: 'P300', 4 + N170: 'N170', 5 + SSVEP: 'SSVEP', 6 + STROOP: 'STROOP', 7 + CUSTOM: 'CUSTOM' 8 8 }; 9 9 10 10 export const SCREENS = { ··· 110 110 }; 111 111 112 112 export const SIGNAL_QUALITY = { 113 - BAD: '#E06766', 113 + BAD: '#ed5a5a', 114 114 OK: '#FFCD39', 115 115 GREAT: '#66B0A9', 116 116 DISCONNECTED: '#BFBFBF'
+1 -1
app/constants/interfaces.js
··· 13 13 14 14 export type ExperimentParameters = { 15 15 trialDuration: number, 16 - experimentDuration: number, 16 + nbTrials: number, 17 17 iti: number, 18 18 jitter: number, 19 19 sampleType: string,
+4 -4
app/epics/deviceEpics.js
··· 88 88 promise.then( 89 89 devices => devices, 90 90 error => { 91 - console.error('searchMuseEpic: ', error); 91 + toast.error(`"Device Error: " ${error.toString()}`); 92 92 toast.error('Device Error: ', error); 93 93 return []; 94 94 } ··· 108 108 promise.then( 109 109 devices => devices, 110 110 error => { 111 - toast.error('Device Error: ', error); 112 - console.error('searchEpic: ', error); 111 + toast.error(`"Device Error: " ${error.toString()}`); 112 + console.error('searchEpic: ', error.toString()); 113 113 return []; 114 114 } 115 115 ) ··· 245 245 )(action$, state$).pipe( 246 246 catchError(error => 247 247 of(error).pipe( 248 - tap(err => toast.error('Device Error: ', err)), 248 + tap(err => toast.error(`"Device Error: " ${err.toString()}`)), 249 249 map(cleanup) 250 250 ) 251 251 )
+3 -1
app/utils/eeg/emotiv.js
··· 6 6 import { fromEvent } from 'rxjs'; 7 7 import { map, withLatestFrom, share } from 'rxjs/operators'; 8 8 import { addInfo, epoch, bandpassFilter } from '@neurosity/pipes'; 9 + import { toast } from 'react-toastify'; 9 10 import { parseEmotivSignalQuality } from './pipes'; 10 11 import { 11 12 USERNAME, ··· 24 25 25 26 // Gets a list of available Emotiv devices 26 27 export const getEmotiv = async () => { 28 + console.log(client); 27 29 const devices = await client.queryHeadsets(); 28 30 return devices; 29 31 }; ··· 53 55 samplingRate: session.headset.settings.eegRate, 54 56 channels: EMOTIV_CHANNELS 55 57 }), 56 - err => console.log(err) 58 + err => toast('Device Error: ', err) 57 59 ); 58 60 59 61 export const disconnectFromEmotiv = async () => {
+22 -17
app/utils/filesystem/storage.js
··· 3 3 /** 4 4 * Functions for managing user data stored on disk 5 5 */ 6 - import * as fs from "fs"; 7 - import * as os from "os"; 8 - import * as path from "path"; 9 - import recursive from "recursive-readdir"; 10 - import { ExperimentStateType } from "../../reducers/experimentReducer"; 11 - import { mkdirPathSync } from "./write"; 6 + import * as fs from 'fs'; 7 + import * as os from 'os'; 8 + import * as path from 'path'; 9 + import recursive from 'recursive-readdir'; 10 + import { shell } from 'electron'; 11 + import { ExperimentStateType } from '../../reducers/experimentReducer'; 12 + import { mkdirPathSync } from './write'; 12 13 13 - const workspaces = path.join(os.homedir(), "BrainWaves Workspaces"); 14 + const workspaces = path.join(os.homedir(), 'BrainWaves Workspaces'); 14 15 15 16 // ----------------------------------------------------------------------------------------------- 16 17 // Creating and Getting ··· 22 23 // Gets the absolute path for a workspace from a given title 23 24 export const getWorkspaceDir = (title: string) => path.join(workspaces, title); 24 25 26 + // Opens a workspace folder in explorer (or other native OS filesystem browser) 27 + export const openWorkspaceDir = (title: string) => 28 + shell.openItem(path.join(workspaces, title)); 29 + 25 30 // ----------------------------------------------------------------------------------------------- 26 31 // Storing 27 32 28 33 // Writes 'experiment' store state to file as a JSON object 29 34 export const storeExperimentState = (state: ExperimentStateType) => 30 35 fs.writeFileSync( 31 - path.join(getWorkspaceDir(state.title), "appState.json"), 36 + path.join(getWorkspaceDir(state.title), 'appState.json'), 32 37 JSON.stringify(state) 33 38 ); 34 39 ··· 38 43 subject: string, 39 44 session: number 40 45 ) => { 41 - const dir = path.join(getWorkspaceDir(title), "Data", subject, "Behavior"); 46 + const dir = path.join(getWorkspaceDir(title), 'Data', subject, 'Behavior'); 42 47 const filename = `${subject}-${session}-behavior.csv`; 43 48 mkdirPathSync(dir); 44 49 fs.writeFile(path.join(dir, filename), csv, err => { ··· 54 59 imageTitle: string, 55 60 rawData: Buffer 56 61 ) => { 57 - const dir = path.join(getWorkspaceDir(title), "Results", "Images"); 62 + const dir = path.join(getWorkspaceDir(title), 'Results', 'Images'); 58 63 const filename = `${imageTitle}.png`; 59 64 mkdirPathSync(dir); 60 65 fs.writeFile(path.join(dir, filename), rawData, err => { ··· 72 77 try { 73 78 return fs.readdirSync(workspaces); 74 79 } catch (e) { 75 - if (e.code === "ENOENT") { 80 + if (e.code === 'ENOENT') { 76 81 mkdirPathSync(workspaces); 77 82 } 78 83 console.log(e); ··· 86 91 try { 87 92 const files = await recursive(getWorkspaceDir(title)); 88 93 const rawFiles = files 89 - .filter(filepath => filepath.slice(-7).includes("raw.csv")) 94 + .filter(filepath => filepath.slice(-7).includes('raw.csv')) 90 95 .map(filepath => ({ 91 96 name: path.basename(filepath), 92 97 path: filepath 93 98 })); 94 99 return rawFiles; 95 100 } catch (e) { 96 - if (e.code === "ENOENT") { 101 + if (e.code === 'ENOENT') { 97 102 console.log(e); 98 103 return []; 99 104 } ··· 106 111 try { 107 112 const files = await recursive(getWorkspaceDir(title)); 108 113 return files 109 - .filter(filepath => filepath.slice(-7).includes("epo.fif")) 114 + .filter(filepath => filepath.slice(-7).includes('epo.fif')) 110 115 .map(filepath => ({ 111 116 name: path.basename(filepath), 112 117 path: filepath ··· 121 126 export const readAndParseState = (dir: string) => { 122 127 try { 123 128 return JSON.parse( 124 - fs.readFileSync(path.join(workspaces, dir, "appState.json")) 129 + fs.readFileSync(path.join(workspaces, dir, 'appState.json')) 125 130 ); 126 131 } catch (e) { 127 - if (e.code === "ENOENT") { 128 - console.log("appState does not exist for recent workspace"); 132 + if (e.code === 'ENOENT') { 133 + console.log('appState does not exist for recent workspace'); 129 134 } 130 135 return null; 131 136 }
+1 -4
app/utils/jspsych/functions.js
··· 61 61 ], 62 62 sample: { 63 63 type: params.sampleType, 64 - size: Math.round( 65 - params.experimentDuration / 66 - (params.trialDuration + params.iti + params.jitter / 2) 67 - ) 64 + size: params.nbTrials 68 65 }, 69 66 timeline_variables: readdirSync(params.stimulus1.dir) 70 67 .map(filename => ({
+2 -2
app/utils/jspsych/timelines/n170.js
··· 6 6 7 7 const facesDir = path.join(rootFolder, 'assets', 'face_house', 'faces'); 8 8 const housesDir = path.join(rootFolder, 'assets', 'face_house', 'houses'); 9 - const fixation = path.join(rootFolder, 'assets', 'face_house', 'fixation.jpg'); 9 + const fixation = path.join(rootFolder, 'assets', 'common', 'fixationcross.png'); 10 10 11 11 export const buildN170Timeline = () => ({ 12 12 params: { 13 13 trialDuration: 300, 14 - experimentDuration: 120000, 14 + nbTrials: 200, 15 15 iti: 800, 16 16 jitter: 200, 17 17 sampleType: 'with-replacement',
+1 -4
app/utils/jupyter/cells.js
··· 35 35 export const cleanEpochsPlot = () => 36 36 [ 37 37 `%matplotlib`, 38 - `epochs.plot(scalings='auto')`, 39 - `fig = plt.gcf()`, 40 - `fig.canvas.manager.window.activateWindow()`, 41 - `fig.canvas.manager.window.raise_()` 38 + `epochs.plot(scalings='auto', n_epochs=8, title="", events=None)` 42 39 ].join('\n'); 43 40 44 41 export const plotPSD = () =>