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.

fixed bug with muse plotting

+64 -25
+7 -2
app/constants/interfaces.ts
··· 113 113 // -------------------------------------------------------------------- 114 114 // Device 115 115 116 - // TODO: type this based on what comes out of muse and emotiv 116 + // For unconnected available devices 117 117 export interface Device { 118 - [key: string]: any; 118 + // Human readable 119 + name?: string; 120 + // Unique ID 121 + id: string; 119 122 } 120 123 121 124 export interface EEGData { ··· 130 133 timestamp?: number; 131 134 } 132 135 136 + // For connected devices 133 137 export interface DeviceInfo { 134 138 name: string; 135 139 samplingRate: number; 140 + channels: string[]; 136 141 } 137 142 138 143 export interface PipesEpoch {
+2 -1
app/epics/deviceEpics.ts
··· 129 129 action$.pipe( 130 130 filter(isActionOf(DeviceActions.ConnectToDevice)), 131 131 pluck('payload'), 132 - map<Device, Promise<any>>((device) => 132 + map((device) => 133 133 isNil(device.name) ? connectToEmotiv(device) : connectToMuse(device) 134 134 ), 135 135 mergeMap<Promise<any>, ObservableInput<DeviceInfo>>((promise) => ··· 137 137 ), 138 138 mergeMap<DeviceInfo, ObservableInput<any>>((deviceInfo) => { 139 139 if (!isNil(deviceInfo) && !isNil(deviceInfo.samplingRate)) { 140 + console.log(deviceInfo); 140 141 return of( 141 142 DeviceActions.SetDeviceType( 142 143 deviceInfo.name.includes('Muse') ? DEVICES.MUSE : DEVICES.EMOTIV
+2
app/main.dev.ts
··· 76 76 process.env.ERB_SECURE !== 'true' 77 77 ? { 78 78 nodeIntegration: true, 79 + webviewTag: true, 79 80 } 80 81 : { 81 82 preload: path.join(__dirname, 'dist/renderer.prod.js'), 83 + webviewTag: true, 82 84 }, 83 85 }); 84 86
+1 -1
app/reducers/deviceReducer.ts
··· 26 26 } 27 27 28 28 const initialState: DeviceStateType = { 29 - availableDevices: [{}], 29 + availableDevices: [], 30 30 connectedDevice: { name: 'disconnected', samplingRate: 0 }, 31 31 connectionStatus: CONNECTION_STATUS.NOT_YET_CONNECTED, 32 32 deviceAvailability: DEVICE_AVAILABILITY.NONE,
+24 -8
app/utils/eeg/emotiv.ts
··· 11 11 import { CLIENT_ID, CLIENT_SECRET, LICENSE_ID } from '../../../keys'; 12 12 import { EMOTIV_CHANNELS, PLOTTING_INTERVAL } from '../../constants/constants'; 13 13 import Cortex from './cortex'; 14 + import { Device, DeviceInfo } from '../../constants/interfaces'; 15 + 16 + interface EmotivHeadset { 17 + id: string; 18 + status: 'discovered' | 'connecting' | 'connected'; 19 + connectedBy: 'dongle' | 'bluetooth' | 'usb cabe' | 'extender'; 20 + dongle: string; 21 + firmware: string; 22 + motionSensors: string[]; 23 + sensors: string[]; 24 + settings: Record<string, string | number>; 25 + customName?: string; 26 + } 14 27 15 28 // Creates the Cortex object from SDK 16 29 const verbose = process.env.LOG_LEVEL || 1; ··· 26 39 27 40 // Gets a list of available Emotiv devices 28 41 export const getEmotiv = async () => { 29 - const devices = await client.queryHeadsets(); 30 - return devices; 42 + const devices: EmotivHeadset[] = await client.queryHeadsets(); 43 + return devices.map<Device>((headset) => ({ 44 + id: headset.id, 45 + name: headset.customName, 46 + })); 31 47 }; 32 48 33 - export const connectToEmotiv = async (device) => { 49 + export const connectToEmotiv = async ( 50 + device: Device 51 + ): Promise<DeviceInfo | null> => { 34 52 await client.ready; 35 53 36 54 // Authenticate ··· 43 61 }); 44 62 } catch (err) { 45 63 toast.error(`Authentication failed. ${err.message}`); 46 - return; 64 + return Promise.reject(err); 47 65 } 48 66 // Connect 49 67 try { 50 68 await client.controlDevice({ command: 'connect', headset: device.id }); 51 69 } catch (err) { 52 70 toast.error(`Emotiv connection failed. ${err.message}`); 53 - return; 71 + return Promise.reject(err); 54 72 } 55 73 // Create Session 56 74 try { ··· 67 85 }; 68 86 } catch (err) { 69 87 toast.error(`Session creation failed. ${err.message} `); 88 + return Promise.reject(err); 70 89 } 71 90 }; 72 91 73 92 export const disconnectFromEmotiv = async () => { 74 - console.log('disconnecting form emotiv'); 75 93 const sessionStatus = await client.updateSession({ 76 94 session: session.id, 77 95 status: 'close', ··· 93 111 toast.error(`EEG connection failed. ${err.message}`); 94 112 } 95 113 96 - // @ts-ignore 97 114 return fromEvent(client, 'eeg').pipe(map(createEEGSample)); 98 115 }; 99 116 100 117 // Creates an observable that will epoch, filter, and add signal quality to EEG stream 101 118 export const createEmotivSignalQualityObservable = (rawObservable) => { 102 - // @ts-ignore 103 119 const signalQualityObservable = fromEvent(client, 'dev'); 104 120 const samplingRate = 128; 105 121 const channels = EMOTIV_CHANNELS;
+28 -13
app/utils/eeg/muse.ts
··· 1 1 import 'hazardous'; 2 - import { withLatestFrom, share, startWith, filter } from 'rxjs/operators'; 2 + import { 3 + withLatestFrom, 4 + share, 5 + startWith, 6 + filter, 7 + tap, 8 + map, 9 + } from 'rxjs/operators'; 3 10 import { 4 11 addInfo, 5 12 epoch, ··· 7 14 addSignalQuality, 8 15 } from '@neurosity/pipes'; 9 16 import { release } from 'os'; 10 - import { MUSE_SERVICE, MuseClient, zipSamples } from 'muse-js'; 11 - import { from } from 'rxjs'; 17 + import { MUSE_SERVICE, MuseClient, zipSamples, EEGSample } from 'muse-js'; 18 + import { from, Observable } from 'rxjs'; 19 + import { isNaN } from 'lodash'; 12 20 import { parseMuseSignalQuality } from './pipes'; 13 21 import { 14 22 MUSE_SAMPLING_RATE, 15 23 MUSE_CHANNELS, 16 24 PLOTTING_INTERVAL, 17 25 } from '../../constants/constants'; 26 + import { Device, EEGData } from '../../constants/interfaces'; 18 27 19 28 const INTER_SAMPLE_INTERVAL = -(1 / 256) * 1000; 20 29 ··· 27 36 28 37 const client = new MuseClient(); 29 38 client.enableAux = false; 30 - console.log('this', this, 'ble', navigator.bluetooth); 31 39 32 40 // Gets an available Muse device 33 - // TODO: test whether this will ever return multiple devices if available 41 + // TODO: is being able to request only one Muse at a time a problem in a classroom scenario? 34 42 export const getMuse = async () => { 35 - console.log('getting muse'); 36 - const device = await navigator.bluetooth.requestDevice({ 43 + const deviceInstance = await navigator.bluetooth.requestDevice({ 37 44 filters: [{ services: [MUSE_SERVICE] }], 38 45 }); 39 - console.log('received ', device); 40 - return [device]; 46 + return [{ id: deviceInstance.id, name: deviceInstance.name }]; 41 47 }; 42 48 43 49 // Attempts to connect to a muse device. If successful, returns a device info object 44 - export const connectToMuse = async (device: BluetoothDevice) => { 45 - await client.connect(device.gatt); 50 + export const connectToMuse = async (device: Device) => { 51 + const deviceInstance = await navigator.bluetooth.requestDevice({ 52 + filters: [{ services: [MUSE_SERVICE], name: device.name }], 53 + }); 54 + const gatt = await deviceInstance.gatt?.connect(); 55 + await client.connect(gatt); 46 56 return { 47 57 name: client.deviceName, 48 58 samplingRate: MUSE_SAMPLING_RATE, ··· 58 68 const eegStream = await client.eegReadings; 59 69 const markers = await client.eventMarkers.pipe(startWith({ timestamp: 0 })); 60 70 return from(zipSamples(eegStream)).pipe( 61 - filter((sample) => !sample.data.includes(NaN)), 71 + // Remove nans if present (muse 2) 72 + map<EEGSample, EEGSample>((sample) => ({ 73 + ...sample, 74 + data: sample.data.filter((val) => !isNaN(val)), 75 + })), 76 + filter((sample) => sample.data.length >= 4), 62 77 withLatestFrom(markers, synchronizeTimestamp), 63 78 share() 64 79 ); ··· 66 81 67 82 // Creates an observable that will epoch, filter, and add signal quality to EEG stream 68 83 export const createMuseSignalQualityObservable = ( 69 - rawObservable, 84 + rawObservable: Observable<EEGData>, 70 85 deviceInfo 71 86 ) => { 72 87 const { samplingRate, channels: channelNames } = deviceInfo;