An easy-to-use platform for EEG experimentation in the classroom
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;