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.

IMproved error handling for device connection

jdpigeon 2a006290 1f7785b7

+100 -97
+13 -17
app/components/CollectComponent/ConnectModal.js
··· 1 - import React, { Component } from 'react'; 2 - import { isNil, debounce } from 'lodash'; 1 + import React, { Component } from "react"; 2 + import { isNil, debounce } from "lodash"; 3 3 import { 4 4 Modal, 5 5 Button, 6 6 Segment, 7 - // Image, 8 7 List, 9 8 Grid, 10 9 Divider 11 - } from 'semantic-ui-react'; 10 + } from "semantic-ui-react"; 12 11 import { 13 12 DEVICES, 14 13 DEVICE_AVAILABILITY, 15 14 CONNECTION_STATUS, 16 15 SCREENS 17 - } from '../../constants/constants'; 18 - import styles from '../styles/collect.css'; 16 + } from "../../constants/constants"; 17 + import styles from "../styles/collect.css"; 19 18 20 19 interface Props { 21 20 history: Object; ··· 46 45 handleStartTutorial: () => void; 47 46 48 47 static getDeviceName(device: any) { 49 - return isNil(device.name) ? device.id : device.name; 48 + if (!isNil(device)) { 49 + return isNil(device.name) ? device.id : device.name; 50 + } 51 + return ""; 50 52 } 51 53 52 54 constructor(props: Props) { ··· 110 112 link 111 113 name={ 112 114 this.state.selectedDevice === device 113 - ? 'check circle outline' 114 - : 'circle outline' 115 + ? "check circle outline" 116 + : "circle outline" 115 117 } 116 118 size="large" 117 119 verticalAlign="middle" ··· 131 133 if (this.props.deviceAvailability === DEVICE_AVAILABILITY.SEARCHING) { 132 134 return ( 133 135 <React.Fragment> 134 - {/* <Modal.Content image> 135 - <Image src={blake} size="tiny" centered /> 136 - </Modal.Content> */} 137 136 <Modal.Content className={styles.searchingText}> 138 137 Searching for available headset(s)... 139 138 </Modal.Content> ··· 143 142 if (this.props.connectionStatus === CONNECTION_STATUS.CONNECTING) { 144 143 return ( 145 144 <React.Fragment> 146 - {/* <Modal.Content image> 147 - <Image src={blake} size="tiny" centered /> 148 - </Modal.Content> */} 149 145 <Modal.Content className={styles.searchingText}> 150 - Connecting to{' '} 146 + Connecting to{" "} 151 147 {ConnectModal.getDeviceName(this.state.selectedDevice)} 152 148 ... 153 149 </Modal.Content> ··· 205 201 Insert the USB Receiver 206 202 </Modal.Header> 207 203 <Modal.Content> 208 - Inser the USB receiver into a USB port on your computer. Ensure that 204 + Insert the USB receiver into a USB port on your computer. Ensure that 209 205 the LED on the receiver is continously lit or flickering rapidly. If 210 206 it is blinking slowly or not illuminated, remove and reinsert the 211 207 receiver
+3 -4
app/components/HomeComponent/index.js
··· 171 171 <Image src={faceHouseIcon} /> 172 172 <Header as="h1">Faces and Houses</Header> 173 173 <p> 174 - Explore the N170 ERP that is produce in response to viewing 175 - faces (as compared to non human objects). It is called the 176 - N170 because it is a negative deflection that occurs around 177 - 170ms after perceiving a face. 174 + Explore the N170 event-related potential that is produced in response to viewing 175 + faces. It is called the N170 because it is a negative, downwards-facing wave that occurs around 176 + 170 milliseconds after perceiving a face. 178 177 </p> 179 178 <Button 180 179 secondary
+37 -43
app/epics/deviceEpics.js
··· 1 - import { combineEpics } from 'redux-observable'; 2 - import { of, from, timer } from 'rxjs'; 3 - import { map, pluck, mergeMap, tap, filter, catchError } from 'rxjs/operators'; 4 - import { isNil } from 'lodash'; 5 - import { toast } from 'react-toastify'; 1 + import { combineEpics } from "redux-observable"; 2 + import { of, from, timer } from "rxjs"; 3 + import { map, pluck, mergeMap, tap, filter, catchError } from "rxjs/operators"; 4 + import { isNil } from "lodash"; 5 + import { toast } from "react-toastify"; 6 6 import { 7 7 CONNECT_TO_DEVICE, 8 8 SET_DEVICE_AVAILABILITY, 9 9 setDeviceAvailability, 10 10 setConnectionStatus 11 - } from '../actions/deviceActions'; 11 + } from "../actions/deviceActions"; 12 12 import { 13 13 getEmotiv, 14 14 connectToEmotiv, 15 15 createRawEmotivObservable, 16 16 createEmotivSignalQualityObservable, 17 17 disconnectFromEmotiv 18 - } from '../utils/eeg/emotiv'; 18 + } from "../utils/eeg/emotiv"; 19 19 import { 20 20 getMuse, 21 21 connectToMuse, 22 22 createRawMuseObservable, 23 23 createMuseSignalQualityObservable, 24 24 disconnectFromMuse 25 - } from '../utils/eeg/muse'; 25 + } from "../utils/eeg/muse"; 26 26 import { 27 27 CONNECTION_STATUS, 28 28 DEVICES, 29 29 DEVICE_AVAILABILITY, 30 30 SEARCH_TIMER 31 - } from '../constants/constants'; 32 - import { EXPERIMENT_CLEANUP } from './experimentEpics'; 31 + } from "../constants/constants"; 32 + import { EXPERIMENT_CLEANUP } from "./experimentEpics"; 33 33 34 - export const DEVICE_FOUND = 'DEVICE_FOUND'; 35 - export const SET_DEVICE_TYPE = 'DEVICE_TYPE'; 36 - export const SET_CONNECTION_STATUS = 'SET_CONNECTION_STATUS'; 37 - export const SET_DEVICE_INFO = 'SET_DEVICE_INFO'; 38 - export const SET_AVAILABLE_DEVICES = 'SET_AVAILABLE_DEVICES'; 39 - export const SET_RAW_OBSERVABLE = 'SET_RAW_OBSERVABLE'; 40 - export const SET_SIGNAL_OBSERVABLE = 'SET_SIGNAL_OBSERVABLE'; 41 - export const DEVICE_CLEANUP = 'DEVICE_CLEANUP'; 34 + export const DEVICE_FOUND = "DEVICE_FOUND"; 35 + export const SET_DEVICE_TYPE = "DEVICE_TYPE"; 36 + export const SET_CONNECTION_STATUS = "SET_CONNECTION_STATUS"; 37 + export const SET_DEVICE_INFO = "SET_DEVICE_INFO"; 38 + export const SET_AVAILABLE_DEVICES = "SET_AVAILABLE_DEVICES"; 39 + export const SET_RAW_OBSERVABLE = "SET_RAW_OBSERVABLE"; 40 + export const SET_SIGNAL_OBSERVABLE = "SET_SIGNAL_OBSERVABLE"; 41 + export const DEVICE_CLEANUP = "DEVICE_CLEANUP"; 42 42 43 43 // ------------------------------------------------------------------------- 44 44 // Action Creators ··· 78 78 // ------------------------------------------------------------------------- 79 79 // Epics 80 80 81 - // NOTE: Uses a Promise.then inside b/c from leads to loss of user gesture propagation for web bluetooth 81 + // NOTE: Uses a Promise "then" inside b/c Observable.from leads to loss of user gesture propagation for web bluetooth 82 82 const searchMuseEpic = action$ => 83 83 action$.ofType(SET_DEVICE_AVAILABILITY).pipe( 84 - pluck('payload'), 84 + pluck("payload"), 85 85 filter(status => status === DEVICE_AVAILABILITY.SEARCHING), 86 86 map(getMuse), 87 - tap(console.log), 88 87 mergeMap(promise => 89 88 promise.then( 90 89 devices => devices, ··· 101 100 102 101 const searchEmotivEpic = action$ => 103 102 action$.ofType(SET_DEVICE_AVAILABILITY).pipe( 104 - pluck('payload'), 103 + pluck("payload"), 105 104 filter(status => status === DEVICE_AVAILABILITY.SEARCHING), 106 - filter(() => process.platform === 'darwin' || process.platform === 'win32'), 105 + filter(() => process.platform === "darwin" || process.platform === "win32"), 107 106 map(getEmotiv), 108 107 mergeMap(promise => 109 108 promise.then( 110 109 devices => devices, 111 110 error => { 112 - if (error.toString().contains('client.queryHeadsets')) { 113 - toast.error('Device Error: Could not find Cortex Service'); 111 + if (error.message.includes("client.queryHeadsets")) { 112 + toast.error( 113 + "Could not connect to Cortex Service. Please connect to the internet and install Cortex to use Emotiv EEG", 114 + { autoclose: 7000 } 115 + ); 114 116 } else { 115 117 toast.error(`"Device Error: " ${error.toString()}`); 116 118 } 117 - console.error('searchEpic: ', error.toString()); 119 + console.error("searchEpic: ", error.toString()); 118 120 return []; 119 121 } 120 122 ) ··· 125 127 126 128 const deviceFoundEpic = (action$, state$) => 127 129 action$.ofType(DEVICE_FOUND).pipe( 128 - pluck('payload'), 130 + pluck("payload"), 129 131 map(foundDevices => 130 132 foundDevices.reduce((acc, curr) => { 131 133 if (acc.find(device => device.id === curr.id)) { ··· 144 146 145 147 const searchTimerEpic = (action$, state$) => 146 148 action$.ofType(SET_DEVICE_AVAILABILITY).pipe( 147 - pluck('payload'), 149 + pluck("payload"), 148 150 filter(status => status === DEVICE_AVAILABILITY.SEARCHING), 149 151 mergeMap(() => timer(SEARCH_TIMER)), 150 152 filter( ··· 156 158 157 159 const connectEpic = action$ => 158 160 action$.ofType(CONNECT_TO_DEVICE).pipe( 159 - pluck('payload'), 160 - map( 161 - device => 162 - isNil(device.name) ? connectToEmotiv(device) : connectToMuse(device) 163 - ), 164 - mergeMap(promise => 165 - promise.then( 166 - deviceInfo => deviceInfo, 167 - error => { 168 - toast.error(`"Device Error: " ${error.toString()}`); 169 - } 170 - ) 161 + pluck("payload"), 162 + map(device => 163 + isNil(device.name) ? connectToEmotiv(device) : connectToMuse(device) 171 164 ), 165 + mergeMap(promise => promise.then(deviceInfo => deviceInfo)), 172 166 mergeMap(deviceInfo => { 173 - if (!isNil(deviceInfo.name)) { 167 + if (!isNil(deviceInfo.samplingRate)) { 174 168 return of( 175 169 setDeviceType( 176 - deviceInfo.name.includes('Muse') ? DEVICES.MUSE : DEVICES.EMOTIV 170 + deviceInfo.name.includes("Muse") ? DEVICES.MUSE : DEVICES.EMOTIV 177 171 ), 178 172 setDeviceInfo(deviceInfo), 179 173 setConnectionStatus(CONNECTION_STATUS.CONNECTED) ··· 201 195 202 196 const setSignalQualityObservableEpic = (action$, state$) => 203 197 action$.ofType(SET_RAW_OBSERVABLE).pipe( 204 - pluck('payload'), 198 + pluck("payload"), 205 199 map(rawObservable => { 206 200 if (state$.value.device.deviceType === DEVICES.EMOTIV) { 207 201 return createEmotivSignalQualityObservable(
+2 -2
app/utils/eeg/cortex.js
··· 6 6 * We use it both in the browser and NodeJS code. 7 7 * 8 8 * It makes extensive use of Promises for flow control; all requests return a 9 - * Promise with their result. 10 - * 9 + * Promise with their result. 10 + * 11 11 * For the subscription types in Cortex, we use an event emitter. Each kind of 12 12 * event (mot, eeg, etc) is emitted as its own event that you can listen for 13 13 * whether or not there are any active subscriptions at the time.
+37 -22
app/utils/eeg/emotiv.js
··· 3 3 * an RxJS Observable of raw EEG data 4 4 * 5 5 */ 6 - import { fromEvent } from 'rxjs'; 7 - import { map, withLatestFrom, share } from 'rxjs/operators'; 8 - import { addInfo, epoch, bandpassFilter } from '@neurosity/pipes'; 9 - import { toast } from 'react-toastify'; 10 - import { parseEmotivSignalQuality } from './pipes'; 6 + import { fromEvent } from "rxjs"; 7 + import { map, withLatestFrom, share } from "rxjs/operators"; 8 + import { addInfo, epoch, bandpassFilter } from "@neurosity/pipes"; 9 + import { toast } from "react-toastify"; 10 + import { parseEmotivSignalQuality } from "./pipes"; 11 11 import { 12 12 USERNAME, 13 13 PASSWORD, 14 14 CLIENT_ID, 15 15 CLIENT_SECRET, 16 16 LICENSE_ID 17 - } from '../../../keys'; 18 - import { EMOTIV_CHANNELS, PLOTTING_INTERVAL } from '../../constants/constants'; 19 - import Cortex from './cortex'; 17 + } from "../../../keys"; 18 + import { EMOTIV_CHANNELS, PLOTTING_INTERVAL } from "../../constants/constants"; 19 + import Cortex from "./cortex"; 20 20 21 - // Just returns the Cortex object from SDK 21 + // Creates the Cortex object from SDK 22 22 const verbose = process.env.LOG_LEVEL || 1; 23 23 const options = { verbose }; 24 24 const client = new Cortex(options); ··· 42 42 debit: 1 43 43 }) 44 44 ) 45 + .catch(err => { 46 + toast.error(`Authentication failed. ${err}`); 47 + return err; 48 + }) 45 49 .then(() => 46 50 client.createSession({ 47 - status: 'active', 51 + status: "active", 48 52 headset: device.id 49 53 }) 50 54 ) 51 - .then(session => ({ 52 - name: session.headset.id, 53 - samplingRate: session.headset.settings.eegRate, 54 - channels: EMOTIV_CHANNELS 55 - })) 55 + .catch(err => { 56 + toast.error(`Session creation failed. ${err} `); 57 + return err; 58 + }) 59 + .then(session => { 60 + if (session.headset === undefined) { 61 + return new Error("Session does not exist"); 62 + } 63 + return { 64 + name: session.headset.id, 65 + samplingRate: session.headset.settings.eegRate, 66 + channels: EMOTIV_CHANNELS 67 + }; 68 + }) 56 69 .catch(err => { 57 - toast.error(`Couldn't connect to device ${device.id}`); 70 + toast.error(`Couldn't connect to device ${device.id}: ${err}`); 58 71 return err; 59 72 }); 60 73 61 74 export const disconnectFromEmotiv = async () => { 62 - const sessionStatus = await client.updateSession({ status: 'close' }); 75 + const sessionStatus = await client.updateSession({ status: "close" }); 63 76 return sessionStatus; 64 77 }; 65 78 66 79 // Returns an observable that will handle both connecting to Client and providing a source of EEG data 67 80 export const createRawEmotivObservable = async () => { 68 - const subs = await client.subscribe({ streams: ['eeg', 'dev'] }); 69 - if (!subs[0].eeg) throw new Error('failed to subscribe'); 70 - return fromEvent(client, 'eeg').pipe(map(createEEGSample)); 81 + const subs = await client.subscribe({ streams: ["eeg", "dev"] }); 82 + if (!subs[0].eeg) { 83 + toast.error(`Subscription to Session data failed`); 84 + } 85 + return fromEvent(client, "eeg").pipe(map(createEEGSample)); 71 86 }; 72 87 73 88 // Creates an observable that will epoch, filter, and add signal quality to EEG stream 74 89 export const createEmotivSignalQualityObservable = rawObservable => { 75 - const signalQualityObservable = fromEvent(client, 'dev'); 90 + const signalQualityObservable = fromEvent(client, "dev"); 76 91 const samplingRate = 128; 77 92 const channels = EMOTIV_CHANNELS; 78 93 const intervalSamples = (PLOTTING_INTERVAL * samplingRate) / 1000; ··· 97 112 }; 98 113 99 114 export const injectEmotivMarker = (value, time) => { 100 - client.injectMarker({ label: 'event', value, time }); 115 + client.injectMarker({ label: "event", value, time }); 101 116 }; 102 117 103 118 // ---------------------------------------------------------------------
+8 -9
app/utils/eeg/muse.js
··· 7 7 addSignalQuality 8 8 } from "@neurosity/pipes"; 9 9 import { release } from "os"; 10 - import { MUSE_SERVICE, MuseClient, zipSamples } from "muse-js" 11 - import { from } from "rxjs" 10 + import { MUSE_SERVICE, MuseClient, zipSamples } from "muse-js"; 11 + import { from } from "rxjs"; 12 12 import { parseMuseSignalQuality } from "./pipes"; 13 13 import { 14 14 MUSE_SAMPLING_RATE, ··· 18 18 19 19 const INTER_SAMPLE_INTERVAL = -(1 / 256) * 1000; 20 20 21 - let bluetooth = {} 21 + let bluetooth = {}; 22 22 let client = {}; 23 23 if (process.platform !== "win32" || release().split(".")[0] >= 10) { 24 24 // Just returns the client object from Muse JS ··· 32 32 let device = {}; 33 33 if (process.platform === "win32") { 34 34 if (release().split(".")[0] < 10) { 35 - console.log('win 7 ') 36 - return null 35 + console.log("win 7 "); 36 + return null; 37 37 } 38 - device = await bluetooth.requestDevice({ 39 - filters: [{ services: [MUSE_SERVICE] }] 40 - }); 41 - 38 + device = await bluetooth.requestDevice({ 39 + filters: [{ services: [MUSE_SERVICE] }] 40 + }); 42 41 } else { 43 42 device = await navigator.bluetooth.requestDevice({ 44 43 filters: [{ services: [MUSE_SERVICE] }]