An easy-to-use platform for EEG experimentation in the classroom
at main 204 lines 5.8 kB view raw
1/* 2 * JS Cortex Wrapper 3 * ***************** 4 * 5 * This library is intended to make working with Cortex easier in Javascript. 6 * We use it both in the browser and NodeJS code. 7 * 8 * It makes extensive use of Promises for flow control; all requests return a 9 * Promise with their result. 10 * 11 * For the subscription types in Cortex, we use an event emitter. Each kind of 12 * event (mot, eeg, etc) is emitted as its own event that you can listen for 13 * whether or not there are any active subscriptions at the time. 14 * 15 * The API methods are defined by using Cortex"s inspectApi call. We mostly 16 * just pass information back and forth without doing much with it, with the 17 * exception of the login/auth flow, which we expose as the init() method. 18 */ 19// const WebSocket = require('ws'); 20import { EventEmitter } from 'events'; 21 22const CORTEX_URL = 'wss://localhost:6868'; 23 24const safeParse = (msg) => { 25 try { 26 return JSON.parse(msg); 27 } catch (_) { 28 return null; 29 } 30}; 31 32if (typeof process !== 'undefined' && process.env) { 33 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 34} 35 36class JSONRPCError extends Error { 37 constructor(err) { 38 super(err.message); 39 this.name = this.constructor.name; 40 this.message = err.message; 41 this.code = err.code; 42 } 43 44 toString() { 45 return `${super.toString()} (${this.code})`; 46 } 47} 48 49export default class Cortex extends EventEmitter { 50 constructor(options = {}) { 51 super(); 52 this.options = options; 53 this.ws = new WebSocket(CORTEX_URL); 54 this.msgId = 0; 55 this.requests = {}; 56 this.streams = {}; 57 this.ws.addEventListener('message', this._onmsg.bind(this)); 58 this.ws.addEventListener('close', () => { 59 this._log('ws: Socket closed'); 60 }); 61 this.verbose = options.verbose !== null ? options.verbose : 1; 62 this.handleError = (error) => { 63 throw new JSONRPCError(error); 64 }; 65 66 this.ready = new Promise( 67 (resolve) => this.ws.addEventListener('open', resolve), 68 this.handleError 69 ) 70 .then(() => this._log('ws: Socket opened')) 71 .then(() => this.call('inspectApi')) 72 .then((methods) => { 73 methods.forEach((m) => { 74 this.defineMethod(m.methodName, m.params); 75 }); 76 this._log(`rpc: Added ${methods.length} methods from inspectApi`); 77 return methods; 78 }); 79 } 80 81 _onmsg(msg) { 82 const data = safeParse(msg.data); 83 if (!data) return this._warn('unparseable message', msg); 84 85 this._debug('ws: <-', msg.data); 86 87 if ('id' in data) { 88 const { id } = data; 89 this._log( 90 `[${id}] <-`, 91 data.result ? 'success' : `error (${data.error.message})` 92 ); 93 if (this.requests[id]) { 94 this.requests[id](data.error, data.result); 95 } else { 96 this._warn('rpc: Got response for unknown id', id); 97 } 98 } else if ('sid' in data) { 99 const dataKeys = Object.keys(data).filter( 100 (k) => k !== 'sid' && k !== 'time' && Array.isArray(data[k]) 101 ); 102 dataKeys.forEach( 103 (k) => 104 this.emit(k, data) || this._warn('no listeners for stream event', k) 105 ); 106 } else { 107 this._log('rpc: Unrecognised data', data); 108 } 109 } 110 111 _warn(...msg) { 112 if (this.verbose > 0) console.warn('[Cortex WARN]', ...msg); 113 } 114 115 _log(...msg) { 116 if (this.verbose > 1) console.log('[Cortex LOG]', ...msg); 117 } 118 119 _debug(...msg) { 120 if (this.verbose > 2) console.debug('[Cortex DEBUG]', ...msg); 121 } 122 123 init({ clientId, clientSecret, license, debit } = {}) { 124 const token = this.getUserLogin() 125 .then((users) => { 126 if (users.length === 0) { 127 return Promise.reject(new Error('No logged in user')); 128 } 129 return this.requestAccess({ clientId, clientSecret }); 130 }) 131 .then(({ accessGranted }) => { 132 if (!accessGranted) { 133 return Promise.reject( 134 new Error('Please approve this application in the EMOTIV app') 135 ); 136 } 137 return this.authorize({ 138 clientId, 139 clientSecret, 140 license, 141 debit, 142 }).then(({ cortexToken }) => { 143 this._log('init: Got auth token'); 144 this._debug('init: Auth token', cortexToken); 145 this.cortexToken = cortexToken; 146 return cortexToken; 147 }); 148 }); 149 150 return token; 151 } 152 153 close() { 154 return new Promise((resolve) => { 155 this.ws.close(); 156 this.ws.once('close', resolve); 157 }); 158 } 159 160 call(method, params = {}) { 161 const id = this.msgId++; 162 const msg = JSON.stringify({ jsonrpc: '2.0', method, params, id }); 163 this.ws.send(msg); 164 this._log(`[${id}] -> ${method}`); 165 166 this._debug('ws: ->', msg); 167 return new Promise((resolve, reject) => { 168 this.requests[id] = (err, data) => { 169 delete this.requests[id]; 170 this._debug('rpc: err', err, 'data', data); 171 if (err) return reject(new JSONRPCError(err)); 172 if (data) return resolve(data); 173 return reject(new Error('Invalid JSON-RPC response')); 174 }; 175 }); 176 } 177 178 defineMethod(methodName, paramDefs = []) { 179 if (this[methodName]) return; 180 const needsAuth = paramDefs.some((p) => p.name === 'cortexToken'); 181 const requiredParams = paramDefs 182 .filter((p) => p.required) 183 .map((p) => p.name); 184 185 this[methodName] = (params = {}) => { 186 if (needsAuth && this.cortexToken && !params.cortexToken) { 187 params = { ...params, cortexToken: this.cortexToken }; 188 } 189 const missingParams = requiredParams.filter((p) => params[p] == null); 190 if (missingParams.length > 0) { 191 return this.handleError( 192 new Error( 193 `Missing required params for ${methodName}: ${missingParams.join( 194 ', ' 195 )}` 196 ) 197 ); 198 } 199 return this.call(methodName, params); 200 }; 201 } 202} 203 204Cortex.JSONRPCError = JSONRPCError;