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.

Converted js epics to ts

authored by

jdpigeon and committed by
Teon L Brooks
8e0fd9ad 67711659

+246 -779
+1 -1
app/actions/index.tsx
··· 1 1 export * from './experimentActions'; 2 - export * from './jupyterActions'; 2 + export * from './pyodideActions'; 3 3 export * from './deviceActions';
-81
app/constants/interfaces.js
··· 1 - /* 2 - * This file contains all the custom types that we use for Flow type checking 3 - */ 4 - 5 - import { EVENTS } from './constants'; 6 - 7 - // TODO: Write interfaces for device objects (Observables, Classes, etc) 8 - 9 - // ------------------------------------------------------------------ 10 - // lab.js Experiment 11 - 12 - export type ExperimentParameters = { 13 - trialDuration: number, 14 - nbTrials: number, 15 - iti: number, 16 - jitter: number, 17 - sampleType: string, 18 - intro: string, 19 - // Setting this to any prevents ridiculous flow runtime errors 20 - showProgessBar: any, 21 - stimulus1: { dir: string, type: EVENTS, title: string, response: string }, 22 - stimulus2: { dir: string, type: EVENTS, title: string, response: string }, 23 - }; 24 - 25 - export type ExperimentDescription = { 26 - question: string, 27 - hypothesis: string, 28 - methods: string, 29 - }; 30 - 31 - // Array of timeline and trial ids that will be presented in experiment 32 - export type MainTimeline = Array<string>; 33 - 34 - // jsPsych trial presented as part of an experiment 35 - export interface Trial { 36 - id: string; 37 - type: string; 38 - stimulus?: string | StimulusVariable; 39 - trial_duration?: number | (() => number); 40 - post_trial_gap?: number; 41 - on_load?: (string) => void | StimulusVariable; 42 - choices?: Array<string>; 43 - } 44 - 45 - // Timeline of jsPsych trials 46 - export type Timeline = { 47 - id: string, 48 - timeline: Array<Trial>, 49 - sample?: SampleParameter, 50 - timeline_variables?: Array<Object>, 51 - }; 52 - 53 - export interface SampleParameter { 54 - type: string; 55 - size?: number; 56 - fn?: () => Array<number>; 57 - } 58 - 59 - export type StimulusVariable = () => any; 60 - 61 - // -------------------------------------------------------------------- 62 - // Device 63 - 64 - export interface EEGData { 65 - data: Array<number>; 66 - timestamp: number; 67 - marker?: string | number; 68 - } 69 - 70 - export interface DeviceInfo { 71 - name: string; 72 - samplingRate: number; 73 - } 74 - 75 - // -------------------------------------------------------------------- 76 - // General 77 - 78 - export interface ActionType { 79 - +payload: any; 80 - +type: string; 81 - }
-23
app/containers/HomeContainer.js
··· 1 - // @flow 2 - import { connect } from 'react-redux'; 3 - import { bindActionCreators } from 'redux'; 4 - import Home from '../components/HomeComponent'; 5 - import * as deviceActions from '../actions/deviceActions'; 6 - import * as pyodideActions from '../actions/pyodideActions'; 7 - import * as experimentActions from '../actions/experimentActions'; 8 - 9 - function mapStateToProps(state) { 10 - return { 11 - availableDevices: state.device.availableDevices, 12 - }; 13 - } 14 - 15 - function mapDispatchToProps(dispatch) { 16 - return { 17 - deviceActions: bindActionCreators(deviceActions, dispatch), 18 - pyodideActions: bindActionCreators(pyodideActions, dispatch), 19 - experimentActions: bindActionCreators(experimentActions, dispatch), 20 - }; 21 - } 22 - 23 - export default connect(mapStateToProps, mapDispatchToProps)(Home);
-407
app/epics/jupyterEpics.ts
··· 1 - import { combineEpics, Epic } from 'redux-observable'; 2 - import { from, of, ObservableInput } from 'rxjs'; 3 - import { 4 - map, 5 - mergeMap, 6 - tap, 7 - pluck, 8 - ignoreElements, 9 - filter, 10 - take, 11 - } from 'rxjs/operators'; 12 - import { find } from 'kernelspecs'; 13 - import { launchSpec } from 'spawnteract'; 14 - import { createMainChannel } from 'enchannel-zmq-backend'; 15 - import { isNil } from 'lodash'; 16 - import { kernelInfoRequest, executeRequest } from '@nteract/messaging'; 17 - import { toast } from 'react-toastify'; 18 - import { isActionOf } from '../utils/redux'; 19 - import { PyodideActions, PyodideActionType } from '../actions'; 20 - import { execute, awaitOkMessage } from '../utils/pyodide/pipes'; 21 - import { RootState } from '../reducers'; 22 - import { getWorkspaceDir } from '../utils/filesystem/storage'; 23 - import { 24 - imports, 25 - utils, 26 - loadCSV, 27 - loadCleanedEpochs, 28 - filterIIR, 29 - epochEvents, 30 - requestEpochsInfo, 31 - requestChannelInfo, 32 - cleanEpochsPlot, 33 - plotPSD, 34 - plotERP, 35 - plotTopoMap, 36 - saveEpochs, 37 - } from '../utils/pyodide/cells'; 38 - import { 39 - EMOTIV_CHANNELS, 40 - EVENTS, 41 - DEVICES, 42 - MUSE_CHANNELS, 43 - PYODIDE_VARIABLE_NAMES, 44 - } from '../constants/constants'; 45 - import { 46 - parseSingleQuoteJSON, 47 - debugParseMessage, 48 - } from '../utils/pyodide/functions'; 49 - 50 - // ------------------------------------------------------------------------- 51 - // Epics 52 - 53 - const launchEpic: Epic<PyodideActionType, PyodideActionType, RootState> = ( 54 - action$ 55 - ) => 56 - action$.pipe( 57 - filter(isActionOf(PyodideActions.LaunchKernel)), 58 - mergeMap(() => from(find('brainwaves'))), 59 - tap((kernelInfo) => { 60 - if (isNil(kernelInfo)) { 61 - toast.error( 62 - "Could not find 'brainwaves' jupyter kernel. Have you installed Python?" 63 - ); 64 - } 65 - }), 66 - filter((kernelInfo) => !isNil(kernelInfo)), 67 - mergeMap<any, ObservableInput<any>>((kernelInfo) => 68 - from( 69 - launchSpec(kernelInfo.spec, { 70 - // No STDIN, opt in to STDOUT and STDERR as node streams 71 - stdio: ['ignore', 'pipe', 'pipe'], 72 - }) 73 - ) 74 - ), 75 - tap((kernel) => { 76 - // Route everything that we won't get in messages to our own stdout 77 - kernel.spawn.stdout.on('data', (data) => { 78 - const text = data.toString(); 79 - console.log('KERNEL STDOUT: ', text); 80 - }); 81 - kernel.spawn.stderr.on('data', (data) => { 82 - const text = data.toString(); 83 - console.log('KERNEL STDERR: ', text); 84 - toast.error('Jupyter: ', text); 85 - }); 86 - 87 - kernel.spawn.on('close', () => { 88 - console.log('Kernel closed'); 89 - }); 90 - }), 91 - map(JupyterActions.SetKernel) 92 - ); 93 - 94 - const setUpChannelEpic: Epic< 95 - JupyterActionType, 96 - JupyterActionType, 97 - RootState 98 - > = (action$) => 99 - action$.pipe( 100 - filter(isActionOf(JupyterActions.SetKernel)), 101 - pluck('payload'), 102 - mergeMap((kernel) => from(createMainChannel(kernel.config))), 103 - tap((mainChannel) => mainChannel.next(executeRequest(imports()))), 104 - tap((mainChannel) => mainChannel.next(executeRequest(utils()))), 105 - map(JupyterActions.SetMainChannel) 106 - ); 107 - 108 - const receiveChannelMessageEpic: Epic< 109 - JupyterActionType, 110 - JupyterActionType, 111 - RootState 112 - > = (action$, state$) => 113 - action$.pipe( 114 - filter(isActionOf(JupyterActions.SetMainChannel)), 115 - mergeMap<Record<string, unknown>, ObservableInput<JupyterActionType>>(() => 116 - state$.value.jupyter.mainChannel.pipe( 117 - map<{ header: { msg_type: string } }, JupyterActionType>((msg) => { 118 - console.log(debugParseMessage(msg)); 119 - switch (msg.header.msg_type) { 120 - case 'kernel_info_reply': 121 - return JupyterActions.SetKernelInfo(msg); 122 - case 'status': 123 - return JupyterActions.SetKernelStatus(parseKernelStatus(msg)); 124 - case 'stream': 125 - return JupyterActions.ReceiveStream(msg); 126 - case 'execute_reply': 127 - return JupyterActions.ReceiveExecuteReply(msg); 128 - case 'execute_result': 129 - return JupyterActions.ReceiveExecuteResult(msg); 130 - case 'display_data': 131 - default: 132 - return JupyterActions.ReceiveDisplayData(msg); 133 - } 134 - }) 135 - ) 136 - ) 137 - ); 138 - 139 - const requestKernelInfoEpic: Epic< 140 - JupyterActionType, 141 - JupyterActionType, 142 - RootState 143 - > = (action$, state$) => 144 - action$.pipe( 145 - filter(isActionOf(JupyterActions.RequestKernelInfo)), 146 - filter(() => state$.value.jupyter.mainChannel), 147 - map(() => state$.value.jupyter.mainChannel.next(kernelInfoRequest())), 148 - ignoreElements() 149 - ); 150 - 151 - const loadEpochsEpic: Epic<JupyterActionType, JupyterActionType, RootState> = ( 152 - action$, 153 - state$ 154 - ) => 155 - // @ts-expect-error 156 - action$.pipe( 157 - filter(isActionOf(JupyterActions.LoadEpochs)), 158 - pluck('payload'), 159 - filter((filePathsArray) => filePathsArray.length >= 1), 160 - map((filePathsArray) => 161 - state$.value.jupyter.mainChannel.next( 162 - executeRequest(loadCSV(filePathsArray)) 163 - ) 164 - ), 165 - awaitOkMessage(action$), 166 - execute(filterIIR(1, 30), state$), 167 - awaitOkMessage(action$), 168 - map(() => { 169 - if (!state$.value.experiment.params?.stimuli) { 170 - return {}; 171 - } 172 - 173 - return epochEvents( 174 - Object.fromEntries( 175 - state$.value.experiment.params?.stimuli.map((stimulus, i) => [ 176 - stimulus.title, 177 - i, 178 - ]) 179 - ), 180 - -0.1, 181 - 0.8 182 - ); 183 - }), 184 - tap((e) => { 185 - console.log('e', e); 186 - }), 187 - map((epochEventsCommand) => 188 - state$.value.jupyter.mainChannel.next(executeRequest(epochEventsCommand)) 189 - ), 190 - awaitOkMessage(action$), 191 - map(() => JupyterActions.GetEpochsInfo(JUPYTER_VARIABLE_NAMES.RAW_EPOCHS)) 192 - ); 193 - 194 - const loadCleanedEpochsEpic: Epic< 195 - JupyterActionType, 196 - JupyterActionType, 197 - RootState 198 - > = (action$, state$) => 199 - action$.pipe( 200 - filter(isActionOf(JupyterActions.LoadCleanedEpochs)), 201 - pluck('payload'), 202 - filter((filePathsArray) => filePathsArray.length >= 1), 203 - map((filePathsArray) => 204 - state$.value.jupyter.mainChannel.next( 205 - executeRequest(loadCleanedEpochs(filePathsArray)) 206 - ) 207 - ), 208 - awaitOkMessage(action$), 209 - mergeMap(() => 210 - of( 211 - JupyterActions.GetEpochsInfo(JUPYTER_VARIABLE_NAMES.CLEAN_EPOCHS), 212 - JupyterActions.GetChannelInfo(), 213 - JupyterActions.LoadTopo() 214 - ) 215 - ) 216 - ); 217 - 218 - const cleanEpochsEpic: Epic<JupyterActionType, JupyterActionType, RootState> = ( 219 - action$, 220 - state$ 221 - ) => 222 - action$.pipe( 223 - filter(isActionOf(JupyterActions.CleanEpochs)), 224 - execute(cleanEpochsPlot(), state$), 225 - mergeMap(() => 226 - action$.ofType(JupyterActions.ReceiveStream.type).pipe( 227 - pluck('payload'), 228 - filter( 229 - (msg) => 230 - msg.channel === 'iopub' && 231 - msg.content.text.includes('Channels marked as bad') 232 - ), 233 - take(1) 234 - ) 235 - ), 236 - map(() => 237 - state$.value.jupyter.mainChannel.next( 238 - executeRequest( 239 - saveEpochs( 240 - getWorkspaceDir(state$.value.experiment.title!), 241 - state$.value.experiment.subject 242 - ) 243 - ) 244 - ) 245 - ), 246 - awaitOkMessage(action$), 247 - map(() => JupyterActions.GetEpochsInfo(JUPYTER_VARIABLE_NAMES.RAW_EPOCHS)) 248 - ); 249 - 250 - const getEpochsInfoEpic: Epic< 251 - JupyterActionType, 252 - JupyterActionType, 253 - RootState 254 - > = (action$, state$) => 255 - action$.pipe( 256 - filter(isActionOf(JupyterActions.GetEpochsInfo)), 257 - pluck('payload'), 258 - map((variableName) => 259 - state$.value.jupyter.mainChannel.next( 260 - executeRequest(requestEpochsInfo(variableName)) 261 - ) 262 - ), 263 - mergeMap(() => 264 - action$.ofType(JupyterActions.ReceiveExecuteReply.type).pipe( 265 - pluck('payload'), 266 - filter((msg) => msg.channel === 'iopub' && !isNil(msg.content.data)), 267 - pluck('content', 'data', 'text/plain'), 268 - filter((msg) => msg.includes('Drop Percentage')), 269 - take(1) 270 - ) 271 - ), 272 - map((epochInfoString) => 273 - parseSingleQuoteJSON(epochInfoString).map((infoObj) => ({ 274 - name: Object.keys(infoObj)[0], 275 - value: infoObj[Object.keys(infoObj)[0]], 276 - })) 277 - ), 278 - map(JupyterActions.SetEpochInfo) 279 - ); 280 - 281 - const getChannelInfoEpic: Epic< 282 - JupyterActionType, 283 - JupyterActionType, 284 - RootState 285 - > = (action$, state$) => 286 - action$.pipe( 287 - filter(isActionOf(JupyterActions.GetChannelInfo)), 288 - execute(requestChannelInfo(), state$), 289 - mergeMap(() => 290 - action$.ofType(JupyterActions.ReceiveExecuteResult.type).pipe( 291 - pluck('payload'), 292 - filter((msg) => msg.channel === 'iopub' && !isNil(msg.content.data)), 293 - pluck('content', 'data', 'text/plain'), // Filter to prevent this from reading requestEpochsInfo returns 294 - filter((msg) => !msg.includes('Drop Percentage')), 295 - take(1) 296 - ) 297 - ), 298 - map((channelInfoString) => 299 - JupyterActions.SetChannelInfo(parseSingleQuoteJSON(channelInfoString)) 300 - ) 301 - ); 302 - 303 - const loadPSDEpic: Epic<JupyterActionType, JupyterActionType, RootState> = ( 304 - action$, 305 - state$ 306 - ) => 307 - action$.pipe( 308 - filter(isActionOf(JupyterActions.LoadPSD)), 309 - execute(plotPSD(), state$), 310 - mergeMap(() => 311 - action$.ofType(JupyterActions.ReceiveDisplayData.type).pipe( 312 - pluck('payload'), // PSD graphs should have two axes 313 - filter((msg) => msg.content.data['text/plain'].includes('2 Axes')), 314 - pluck('content', 'data'), 315 - take(1) 316 - ) 317 - ), 318 - map(JupyterActions.SetPSDPlot) 319 - ); 320 - 321 - const loadTopoEpic: Epic<JupyterActionType, JupyterActionType, RootState> = ( 322 - action$, 323 - state$ 324 - ) => 325 - action$.pipe( 326 - filter(isActionOf(JupyterActions.LoadTopo)), 327 - execute(plotTopoMap(), state$), 328 - mergeMap(() => 329 - action$ 330 - .ofType(JupyterActions.ReceiveDisplayData.type) 331 - .pipe(pluck('payload'), pluck('content', 'data'), take(1)) 332 - ), 333 - mergeMap((topoPlot) => 334 - of( 335 - JupyterActions.SetTopoPlot(topoPlot), 336 - JupyterActions.LoadERP( 337 - state$.value.device.deviceType === DEVICES.EMOTIV 338 - ? EMOTIV_CHANNELS[0] 339 - : MUSE_CHANNELS[0] 340 - ) 341 - ) 342 - ) 343 - ); 344 - 345 - const loadERPEpic: Epic<JupyterActionType, JupyterActionType, RootState> = ( 346 - action$, 347 - state$ 348 - ) => 349 - action$.pipe( 350 - filter(isActionOf(JupyterActions.LoadERP)), 351 - pluck('payload'), 352 - map((channelName) => { 353 - if (MUSE_CHANNELS.includes(channelName)) { 354 - return MUSE_CHANNELS.indexOf(channelName); 355 - } 356 - if (EMOTIV_CHANNELS.includes(channelName)) { 357 - return EMOTIV_CHANNELS.indexOf(channelName); 358 - } 359 - console.warn( 360 - 'channel name supplied to loadERPEpic does not belong to either device' 361 - ); 362 - return EMOTIV_CHANNELS[0]; 363 - }), 364 - map((channelIndex) => 365 - state$.value.jupyter.mainChannel.next( 366 - executeRequest(plotERP(channelIndex)) 367 - ) 368 - ), 369 - mergeMap(() => 370 - action$.ofType(JupyterActions.ReceiveDisplayData.type).pipe( 371 - pluck('payload'), // ERP graphs should have 1 axis according to MNE 372 - filter((msg) => msg.content.data['text/plain'].includes('1 Axes')), 373 - pluck('content', 'data'), 374 - take(1) 375 - ) 376 - ), 377 - map(JupyterActions.SetERPPlot) 378 - ); 379 - 380 - const closeKernelEpic: Epic<JupyterActionType, JupyterActionType, RootState> = ( 381 - action$, 382 - state$ 383 - ) => 384 - action$.pipe( 385 - filter(isActionOf(JupyterActions.CloseKernel)), 386 - map(() => { 387 - state$.value.jupyter.kernel?.spawn.kill(); 388 - state$.value.jupyter.mainChannel.complete(); 389 - }), 390 - ignoreElements() 391 - ); 392 - 393 - export default combineEpics( 394 - launchEpic, 395 - setUpChannelEpic, 396 - requestKernelInfoEpic, 397 - receiveChannelMessageEpic, 398 - loadEpochsEpic, 399 - loadCleanedEpochsEpic, 400 - cleanEpochsEpic, 401 - getEpochsInfoEpic, 402 - getChannelInfoEpic, 403 - loadPSDEpic, 404 - loadTopoEpic, 405 - loadERPEpic, 406 - closeKernelEpic 407 - );
-267
app/epics/pyodideEpics.js
··· 1 - import { combineEpics } from 'redux-observable'; 2 - import { of, fromEvent } from 'rxjs'; 3 - import { toast } from 'react-toastify'; 4 - import { map, mergeMap, tap, pluck, filter } from 'rxjs/operators'; 5 - import { getWorkspaceDir } from '../utils/filesystem/storage'; 6 - import { parseSingleQuoteJSON } from '../utils/pyodide/functions'; 7 - import { readFiles } from '../utils/filesystem/read'; 8 - import { 9 - LAUNCH, 10 - LOAD_EPOCHS, 11 - LOAD_CLEANED_EPOCHS, 12 - LOAD_PSD, 13 - LOAD_ERP, 14 - LOAD_TOPO, 15 - CLEAN_EPOCHS, 16 - loadTopo, 17 - loadERP, 18 - } from '../actions/pyodideActions'; 19 - import { 20 - loadPyodide, 21 - loadCSV, 22 - loadCleanedEpochs, 23 - filterIIR, 24 - epochEvents, 25 - requestEpochsInfo, 26 - requestChannelInfo, 27 - cleanEpochsPlot, 28 - plotPSD, 29 - plotERP, 30 - plotTopoMap, 31 - saveEpochs, 32 - } from '../utils/pyodide'; 33 - import { 34 - EMOTIV_CHANNELS, 35 - EVENTS, 36 - DEVICES, 37 - MUSE_CHANNELS, 38 - PYODIDE_VARIABLE_NAMES, 39 - PYODIDE_STATUS, 40 - } from '../constants/constants'; 41 - 42 - export const GET_CHANNEL_INFO = 'GET_CHANNEL_INFO'; 43 - export const GET_EPOCHS_INFO = 'GET_EPOCHS_INFO'; 44 - export const RECEIVE_DISPLAY_DATA = 'RECEIVE_DISPLAY_DATA'; 45 - export const RECEIVE_ERROR = 'RECEIVE_ERROR'; 46 - export const RECEIVE_EXECUTE_REPLY = 'RECEIVE_EXECUTE_REPLY'; 47 - export const RECEIVE_EXECUTE_RESULT = 'RECEIVE_EXECUTE_RESULT'; 48 - export const RECEIVE_MESSAGE = 'RECEIVE_MESSAGE'; 49 - export const RECEIVE_STREAM = 'RECEIVE_STREAM'; 50 - export const SET_CHANNEL_INFO = 'SET_CHANNEL_INFO'; 51 - export const SET_EPOCH_INFO = 'SET_EPOCH_INFO'; 52 - export const SET_ERP_PLOT = 'SET_ERP_PLOT'; 53 - export const SET_MAIN_CHANNEL = 'SET_MAIN_CHANNEL'; 54 - export const SET_PSD_PLOT = 'SET_PSD_PLOT'; 55 - export const SET_PYODIDE_STATUS = 'SET_PYODIDE_STATUS'; 56 - export const SET_PYODIDE_WORKER = 'SET_PYODIDE_WORKER'; 57 - export const SET_TOPO_PLOT = 'SET_TOPO_PLOT'; 58 - 59 - // ------------------------------------------------------------------------- 60 - // Action Creators 61 - 62 - const getEpochsInfo = (payload) => ({ payload, type: GET_EPOCHS_INFO }); 63 - 64 - const getChannelInfo = () => ({ type: GET_CHANNEL_INFO }); 65 - 66 - const setEpochInfo = (payload) => ({ 67 - payload, 68 - type: SET_EPOCH_INFO, 69 - }); 70 - 71 - const setChannelInfo = (payload) => ({ 72 - payload, 73 - type: SET_CHANNEL_INFO, 74 - }); 75 - 76 - const setPSDPlot = (payload) => ({ 77 - payload, 78 - type: SET_PSD_PLOT, 79 - }); 80 - 81 - const setTopoPlot = (payload) => ({ 82 - payload, 83 - type: SET_TOPO_PLOT, 84 - }); 85 - 86 - const setERPPlot = (payload) => ({ 87 - payload, 88 - type: SET_ERP_PLOT, 89 - }); 90 - 91 - const setPyodideStatus = (payload) => ({ 92 - payload, 93 - type: SET_PYODIDE_STATUS, 94 - }); 95 - 96 - const setPyodideWorker = (payload: Worker) => ({ 97 - payload, 98 - type: SET_PYODIDE_WORKER, 99 - }); 100 - 101 - const receivePyodideError = (payload) => ({ 102 - payload, 103 - type: RECEIVE_ERROR, 104 - }); 105 - 106 - const receivePyodideMessage = (payload) => ({ 107 - payload, 108 - type: RECEIVE_MESSAGE, 109 - }); 110 - 111 - // ------------------------------------------------------------------------- 112 - // Epics 113 - 114 - const launchEpic = (action$) => 115 - action$.ofType(LAUNCH).pipe( 116 - tap(() => console.log('launching')), 117 - map(loadPyodide), 118 - map(setPyodideWorker) 119 - ); 120 - 121 - const pyodideError = (action$) => 122 - action$.ofType(SET_PYODIDE_WORKER).pipe( 123 - pluck('payload'), 124 - tap((e) => 125 - toast.error( 126 - `Error in pyodideWorker at ${e.filename}, Line: ${e.lineno}, ${e.message}` 127 - ) 128 - ), 129 - map(receivePyodideError) 130 - ); 131 - 132 - const pyodideMessage = (action$) => 133 - action$.ofType(SET_PYODIDE_WORKER).pipe( 134 - pluck('payload'), 135 - tap((e) => { 136 - const { results, error } = e.data; 137 - if (results && !error) { 138 - toast(`Pyodide: `, results); 139 - } else if (error) { 140 - toast.error('Pyodide: ', error); 141 - } 142 - }), 143 - map(receivePyodideMessage) 144 - ); 145 - 146 - const loadEpochsEpic = (action$, state$) => 147 - action$.ofType(LOAD_EPOCHS).pipe( 148 - pluck('payload'), 149 - filter((filePathsArray) => filePathsArray.length >= 1), 150 - tap((files) => console.log('files:', files)), 151 - map((filePathsArray) => readFiles(filePathsArray)), 152 - tap((csvArray) => console.log('csvs:', csvArray)), 153 - mergeMap((csvArray) => loadCSV(csvArray)), 154 - mergeMap(() => filterIIR(1, 30)), 155 - mergeMap(() => 156 - epochEvents( 157 - { 158 - [state$.value.experiment.params.stimulus1.title]: EVENTS.STIMULUS_1, 159 - [state$.value.experiment.params.stimulus2.title]: EVENTS.STIMULUS_2, 160 - [state$.value.experiment.params.stimulus3.title]: EVENTS.STIMULUS_3, 161 - [state$.value.experiment.params.stimulus4.title]: EVENTS.STIMULUS_4, 162 - }, 163 - -0.1, 164 - 0.8 165 - ) 166 - ), 167 - map(() => getEpochsInfo(PYODIDE_VARIABLE_NAMES.RAW_EPOCHS)) 168 - ); 169 - 170 - const loadCleanedEpochsEpic = (action$) => 171 - action$.ofType(LOAD_CLEANED_EPOCHS).pipe( 172 - pluck('payload'), 173 - filter((filePathsArray) => filePathsArray.length >= 1), 174 - map((filePathsArray) => loadCleanedEpochs(filePathsArray)), 175 - mergeMap(() => 176 - of( 177 - getEpochsInfo(PYODIDE_VARIABLE_NAMES.CLEAN_EPOCHS), 178 - getChannelInfo(), 179 - loadTopo() 180 - ) 181 - ) 182 - ); 183 - 184 - const cleanEpochsEpic = (action$, state$) => 185 - action$.ofType(CLEAN_EPOCHS).pipe( 186 - map(cleanEpochsPlot), 187 - map(() => 188 - saveEpochs( 189 - getWorkspaceDir(state$.value.experiment.title), 190 - state$.value.experiment.subject 191 - ) 192 - ), 193 - map(() => getEpochsInfo(PYODIDE_VARIABLE_NAMES.RAW_EPOCHS)) 194 - ); 195 - 196 - const getEpochsInfoEpic = (action$) => 197 - action$.ofType(GET_EPOCHS_INFO).pipe( 198 - pluck('payload'), 199 - tap((payload) => console.log('payload: ', payload)), 200 - mergeMap(requestEpochsInfo), 201 - map((epochInfoArray) => 202 - epochInfoArray.map((infoObj) => ({ 203 - name: Object.keys(infoObj)[0], 204 - value: infoObj[Object.keys(infoObj)[0]], 205 - })) 206 - ), 207 - map(setEpochInfo) 208 - ); 209 - 210 - const getChannelInfoEpic = (action$) => 211 - action$.ofType(GET_CHANNEL_INFO).pipe( 212 - map(requestChannelInfo), 213 - map((channelInfoString) => 214 - setChannelInfo(parseSingleQuoteJSON(channelInfoString)) 215 - ) 216 - ); 217 - 218 - const loadPSDEpic = (action$) => 219 - action$.ofType(LOAD_PSD).pipe(map(plotPSD), map(setPSDPlot)); 220 - 221 - const loadTopoEpic = (action$, state$) => 222 - action$.ofType(LOAD_TOPO).pipe( 223 - map(plotTopoMap), 224 - mergeMap((topoPlot) => 225 - of( 226 - setTopoPlot(topoPlot), 227 - loadERP( 228 - state$.value.device.deviceType === DEVICES.EMOTIV 229 - ? EMOTIV_CHANNELS[0] 230 - : MUSE_CHANNELS[0] 231 - ) 232 - ) 233 - ) 234 - ); 235 - 236 - const loadERPEpic = (action$) => 237 - action$.ofType(LOAD_ERP).pipe( 238 - pluck('payload'), 239 - map((channelName) => { 240 - if (MUSE_CHANNELS.includes(channelName)) { 241 - return MUSE_CHANNELS.indexOf(channelName); 242 - } 243 - if (EMOTIV_CHANNELS.includes(channelName)) { 244 - return EMOTIV_CHANNELS.indexOf(channelName); 245 - } 246 - console.warn( 247 - 'channel name supplied to loadERPEpic does not belong to either device' 248 - ); 249 - return EMOTIV_CHANNELS[0]; 250 - }), 251 - map((channelIndex) => plotERP(channelIndex)), 252 - map(setERPPlot) 253 - ); 254 - 255 - export default combineEpics( 256 - pyodideError, 257 - pyodideMessage, 258 - launchEpic, 259 - loadEpochsEpic, 260 - loadCleanedEpochsEpic, 261 - cleanEpochsEpic, 262 - getEpochsInfoEpic, 263 - getChannelInfoEpic, 264 - loadPSDEpic, 265 - loadTopoEpic, 266 - loadERPEpic 267 - );
+245
app/epics/pyodideEpics.ts
··· 1 + import { combineEpics, Epic } from 'redux-observable'; 2 + import { of } from 'rxjs'; 3 + import { map, mergeMap, tap, pluck, filter } from 'rxjs/operators'; 4 + import { toast } from 'react-toastify'; 5 + import { isActionOf } from '../utils/redux'; 6 + import { PyodideActions, PyodideActionType } from '../actions'; 7 + import { RootState } from '../reducers'; 8 + import { getWorkspaceDir } from '../utils/filesystem/storage'; 9 + import { 10 + loadCSV, 11 + loadCleanedEpochs, 12 + filterIIR, 13 + epochEvents, 14 + requestEpochsInfo, 15 + requestChannelInfo, 16 + cleanEpochsPlot, 17 + plotPSD, 18 + plotERP, 19 + plotTopoMap, 20 + saveEpochs, 21 + loadPyodide, 22 + } from '../utils/pyodide'; 23 + import { 24 + EMOTIV_CHANNELS, 25 + DEVICES, 26 + MUSE_CHANNELS, 27 + PYODIDE_VARIABLE_NAMES, 28 + } from '../constants/constants'; 29 + import { parseSingleQuoteJSON } from '../utils/pyodide/functions'; 30 + 31 + import { readFiles } from '../utils/filesystem/read'; 32 + 33 + // ------------------------------------------------------------------------- 34 + // Epics 35 + 36 + const launchEpic: Epic<PyodideActionType, PyodideActionType, RootState> = ( 37 + action$ 38 + ) => 39 + action$.pipe( 40 + filter(isActionOf(PyodideActions.Launch)), 41 + tap(() => console.log('launching')), 42 + map(loadPyodide), 43 + map(PyodideActions.SetPyodideWorker) 44 + ); 45 + 46 + const pyodideError: Epic<PyodideActionType, PyodideActionType, RootState> = ( 47 + action$ 48 + ) => 49 + action$.pipe( 50 + filter(isActionOf(PyodideActions.SetPyodideWorker)), 51 + pluck('payload'), 52 + tap((e) => 53 + toast.error( 54 + `Error in pyodideWorker at ${e.filename}, Line: ${e.lineno}, ${e.message}` 55 + ) 56 + ), 57 + map(PyodideActions.ReceiveError) 58 + ); 59 + 60 + const receiveChannelMessageEpic: Epic< 61 + PyodideActionType, 62 + PyodideActionType, 63 + RootState 64 + > = (action$, state$) => 65 + action$.pipe( 66 + filter(isActionOf(PyodideActions.SetPyodideWorker)), 67 + tap((e) => { 68 + const { results, error } = e.data; 69 + if (results && !error) { 70 + toast(`Pyodide: `, results); 71 + } else if (error) { 72 + toast.error('Pyodide: ', error); 73 + } 74 + }), 75 + map(PyodideActions.ReceiveMessage) 76 + ); 77 + 78 + const loadEpochsEpic: Epic<PyodideActionType, PyodideActionType, RootState> = ( 79 + action$, 80 + state$ 81 + ) => 82 + action$.pipe( 83 + filter(isActionOf(PyodideActions.LoadEpochs)), 84 + pluck('payload'), 85 + filter((filePathsArray: string[]) => filePathsArray.length >= 1), 86 + map((filePathsArray) => readFiles(filePathsArray)), 87 + mergeMap((csvArray) => loadCSV(csvArray)), 88 + mergeMap(() => filterIIR(1, 30)), 89 + map(() => { 90 + if (!state$.value.experiment.params?.stimuli) { 91 + return {}; 92 + } 93 + 94 + return epochEvents( 95 + Object.fromEntries( 96 + state$.value.experiment.params?.stimuli.map((stimulus, i) => [ 97 + stimulus.title, 98 + i, 99 + ]) 100 + ), 101 + -0.1, 102 + 0.8 103 + ); 104 + }), 105 + tap((e) => { 106 + console.log('epoched events: ', e); 107 + }), 108 + map(() => PyodideActions.GetEpochsInfo(PYODIDE_VARIABLE_NAMES.RAW_EPOCHS)) 109 + ); 110 + 111 + const loadCleanedEpochsEpic: Epic< 112 + PyodideActionType, 113 + PyodideActionType, 114 + RootState 115 + > = (action$) => 116 + action$.pipe( 117 + filter(isActionOf(PyodideActions.LoadCleanedEpochs)), 118 + pluck('payload'), 119 + filter((filePathsArray) => filePathsArray.length >= 1), 120 + map(loadCleanedEpochs), 121 + mergeMap(() => 122 + of( 123 + PyodideActions.GetEpochsInfo(PYODIDE_VARIABLE_NAMES.CLEAN_EPOCHS), 124 + PyodideActions.GetChannelInfo(), 125 + PyodideActions.LoadTopo() 126 + ) 127 + ) 128 + ); 129 + 130 + const cleanEpochsEpic: Epic<PyodideActionType, PyodideActionType, RootState> = ( 131 + action$, 132 + state$ 133 + ) => 134 + action$.pipe( 135 + filter(isActionOf(PyodideActions.CleanEpochs)), 136 + mergeMap(cleanEpochsPlot), 137 + map(() => 138 + saveEpochs( 139 + getWorkspaceDir(state$.value.experiment.title), 140 + state$.value.experiment.subject 141 + ) 142 + ), 143 + map(() => PyodideActions.GetEpochsInfo(PYODIDE_VARIABLE_NAMES.RAW_EPOCHS)) 144 + ); 145 + 146 + const getEpochsInfoEpic: Epic< 147 + PyodideActionType, 148 + PyodideActionType, 149 + RootState 150 + > = (action$, state$) => 151 + action$.pipe( 152 + filter(isActionOf(PyodideActions.GetEpochsInfo)), 153 + pluck('payload'), 154 + mergeMap(requestEpochsInfo), 155 + map((epochInfoArray) => 156 + epochInfoArray.map((infoObj) => ({ 157 + name: Object.keys(infoObj)[0], 158 + value: infoObj[Object.keys(infoObj)[0]], 159 + })) 160 + ), 161 + map(PyodideActions.SetEpochInfo) 162 + ); 163 + 164 + const getChannelInfoEpic: Epic< 165 + PyodideActionType, 166 + PyodideActionType, 167 + RootState 168 + > = (action$, state$) => 169 + action$.pipe( 170 + filter(isActionOf(PyodideActions.GetChannelInfo)), 171 + mergeMap(requestChannelInfo), 172 + map((channelInfoString) => 173 + PyodideActions.SetChannelInfo(parseSingleQuoteJSON(channelInfoString)) 174 + ) 175 + ); 176 + 177 + const loadPSDEpic: Epic<PyodideActionType, PyodideActionType, RootState> = ( 178 + action$, 179 + state$ 180 + ) => 181 + action$.pipe( 182 + filter(isActionOf(PyodideActions.LoadPSD)), 183 + mergeMap(plotPSD), 184 + map(PyodideActions.SetPSDPlot) 185 + ); 186 + 187 + const loadTopoEpic: Epic<PyodideActionType, PyodideActionType, RootState> = ( 188 + action$, 189 + state$ 190 + ) => 191 + action$.pipe( 192 + filter(isActionOf(PyodideActions.LoadTopo)), 193 + mergeMap(plotTopoMap), 194 + tap((e) => console.log('received topo map: ', e)), 195 + mergeMap((topoPlot) => 196 + of( 197 + PyodideActions.SetTopoPlot(topoPlot), 198 + PyodideActions.LoadERP( 199 + state$.value.device.deviceType === DEVICES.EMOTIV 200 + ? EMOTIV_CHANNELS[0] 201 + : MUSE_CHANNELS[0] 202 + ) 203 + ) 204 + ) 205 + ); 206 + 207 + const loadERPEpic: Epic<PyodideActionType, PyodideActionType, RootState> = ( 208 + action$, 209 + state$ 210 + ) => 211 + action$.pipe( 212 + filter(isActionOf(PyodideActions.LoadERP)), 213 + pluck('payload'), 214 + map((channelName: string) => { 215 + let index: number | null = null; 216 + if (MUSE_CHANNELS.includes(channelName)) { 217 + index = MUSE_CHANNELS.indexOf(channelName); 218 + } 219 + if (EMOTIV_CHANNELS.includes(channelName)) { 220 + index = EMOTIV_CHANNELS.indexOf(channelName); 221 + } 222 + if (index) { 223 + return index; 224 + } 225 + console.warn( 226 + 'channel name supplied to loadERPEpic does not belong to either device' 227 + ); 228 + return parseInt(EMOTIV_CHANNELS[0], 10); 229 + }), 230 + mergeMap(plotERP), 231 + map(PyodideActions.SetERPPlot) 232 + ); 233 + 234 + export default combineEpics( 235 + launchEpic, 236 + receiveChannelMessageEpic, 237 + loadEpochsEpic, 238 + loadCleanedEpochsEpic, 239 + cleanEpochsEpic, 240 + getEpochsInfoEpic, 241 + getChannelInfoEpic, 242 + loadPSDEpic, 243 + loadTopoEpic, 244 + loadERPEpic 245 + );