-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmycyton.js
349 lines (322 loc) · 16.5 KB
/
mycyton.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
// --------------------------------------- Cyton stuff ---------------------------------------
const Cyton = require('@openbci/cyton'); // requires node <= v9
const Ganglion = require('@openbci/ganglion'); // requires node <= v9
const WifiCyton = require('@openbci/wifi'); // requires node <= v9
const OpenBCIUtilities = require('@openbci/utilities'); // requires node <= v9
const k = OpenBCIUtilities.constants;
class MyCyton {
/**
* Function: constructor
* ---------------------
* Create a new MyCyton class given the below event functions; attempt connection with default settings
*
* @param onConnectionStatusChange called when the connection status (connected vs connecting vs not connected) changes
* @param onSample called after a sample is received; collates simple samples into groups and averages them first
*/
constructor(onConnectionStatusChange, onSample) {
// do this when the connection status (connected vs connecting vs not connected) changes
this.onConnectionStatusChange = onConnectionStatusChange;
// do this when a sample is received; collates simple samples into groups and averages them first
this.onSample = onSample;
// array where simple samples are collected so that a number of them can be averaged
// could do with being a ring buffer to save on time complexity
this.savedSamples = [];
// determined by the user, the number of simple samples from savedSamples that will be averaged to generate
// the value used by the key press portion of the program
this.samplesPerAverage = 500;
// we want to make sure we aren't sending an "average of 1" of every single sample, overloading the
// front end, so we wait until it has been a certain (small) amount of time from this saved epoch to send new data
this.lastEmitted = 0;
// internal, keeps track of the last sample time so that if no data is received the program can
// try to reconnect
this.lastSampleTime = 0;
// controlled by user, monitored by all infinite loops so that program can exit as cleanly as possible
this.notTerminated = true;
// controlled by user; in the absence of the specified board type, should the program use simulated data?
this.allowSim = false;
// the user's desired board type
this.boardType = "Cyton";
// if a wifi board, the name of the user's board (required for connection)
this.boardName = "OpenBCI-6D58"
// the thresholds, as determined by the user
this.thresholdTypes = null;
this.thresholdParameters = null;
// start checking for lack of data from the board - will trigger the first attempt to connect
this.checkConnection();
}
/**
* Function: emitSamples
* ---------------------
* Use the large number of recent simple samples received from the board to generate an average based on user
* settings for each channel, then send these averages to the webserver.
*
* this.thresholdParameters contains the variable ("n") number of simple samples used to calculate the average
* types below.
*
* Average types:
* "average": take the absolute mean of the last n simple samples
* "max": take the absolute maximum value of the last n simple samples
* "last": take the most recent simple sample
*/
emitSamples() {
// remember when this was called so that we don't send data too often overloading the front end
this.lastEmitted = Date.now();
// if the thresholds have not been explicitly set by the user, initialize some defaults
if (this.thresholdTypes == null) {
this.thresholdTypes = [];
this.thresholdParameters = [];
for (let i = 0; i < this.savedSamples[0].length; i++) {
// these are the same values that are the defaults in the front-end GUI,
// but they are not explicitly synchronized when the program starts
this.thresholdTypes.push("average");
this.thresholdParameters.push(500);
}
}
// we only want to keep a (constant) finite number of simple samples so that we don't run out of memory
if (this.savedSamples.length > this.samplesPerAverage) this.savedSamples = this.savedSamples.slice(this.savedSamples.length - this.samplesPerAverage);
let calculatedSample = [];
for (let c = 0; c < 8; c++) {
let calculatedSampleChannel = 0;
if (this.thresholdTypes[c] === "average") {
// "average": take the absolute mean of the last n simple samples
for (let i = this.savedSamples.length - 1; i >= Math.max(0, this.savedSamples.length - this.thresholdParameters[c]); i--) calculatedSampleChannel += Math.abs(this.savedSamples[i][c]);
calculatedSampleChannel /= Math.min(this.savedSamples.length, this.thresholdParameters[c]);
} else if (this.thresholdTypes[c] === "max") {
// "max": take the absolute maximum value of the last n simple samples
for (let i = this.savedSamples.length - 1; i >= Math.max(0, this.savedSamples.length - this.thresholdParameters[c]); i--) calculatedSampleChannel = Math.max(calculatedSampleChannel, Math.abs(this.savedSamples[i][c]));
} else if (this.thresholdTypes[c] === "last") {
// "last": take the most recent simple sample
calculatedSampleChannel += Math.abs(this.savedSamples[this.savedSamples.length - 1][c]);
}
calculatedSample.push(calculatedSampleChannel.toFixed(8));
}
// send samples to the web server
this.onSample(calculatedSample);
}
/**
* Function: disconnectBoard
* -------------------------
* Explicitly ensure that the program has no active connection with an external board
*/
disconnectBoard() {
// if the board is defined and connected, then call library function to disconnect it
if (this.ourBoard != null && this.ourBoard.isConnected()) this.ourBoard.disconnect().then(() => {
// set the GUI indicator to 'not connected'
this.onConnectionStatusChange(0);
// if the user isn't trying to end the program
if (!this.notTerminated) {
this.ourBoard = null;
}
});
else this.onConnectionStatusChange(0);
}
/**
* Function: terminate
* -------------------
* Explicitly terminate this class instance
*/
terminate() {
// don't try to reconnect the board
this.notTerminated = false;
this.disconnectBoard();
// as of now, the program doesn't always exit on its own - usually 5 seconds is safe enough, but
// the cause for the lack of termination should be determined
setTimeout(process.exit, 5000);
}
/**
* Function: tryConnectBoard
* -------------------------
* Attempt to connect the board according to the current settings
*/
tryConnectBoard() {
// if the user isn't trying to end the program
if (this.notTerminated) {
// make sure board is disconnected before replacing the object, otherwise causes hardware errors
if (this.ourBoard != null && this.ourBoard.isConnected()) this.ourBoard.disconnect().then(this._tryConnectBoard.bind(this));
else this._tryConnectBoard();
}
}
/**
* Function: _tryConnectBoard
* --------------------------
* Private helper function - attempt to connect the board according to the current settings
*/
_tryConnectBoard() {
// define the onSample function that will be called when the board generates a simple sample
// this is the same for all board types (at least so far)
const onSample = (sample) => {
// uncomment below to see if samples are coming/board is connected properly
// console.log(Date.now());
/** Work with sample */
if (Date.now() - this.startCountTime > 1000) {
this.startCountTime = Date.now();
}
// we are only going to collect the values we care about into this list - the board may also supply
// other values such as accelerometer data; parsing these helps lighten the size of the arrays
this.mySample = [];
// determine the number of channels depending on the device we are using
let numChannels = 8;
if (this.boardType === "Ganglion") numChannels = this.ourBoard.numberOfChannels();
// for each channel
for (let i = 0; i < numChannels; i++) {
// if nonzero, then remember that connectivity is good
if (sample.channelData[i] !== 0) {
this.lastSampleTime = Date.now();
this.onConnectionStatusChange(2);
}
this.mySample.push(sample.channelData[i]);
}
// add the simple sample to the class array
this.savedSamples.push(this.mySample);
// emit samples if we didn't JUST do so; data comes in batches of large numbers of simple samples,
// so onSample is called many times in succession, so we should introduce delay between evaluating
// averages
if (Date.now() - this.lastEmitted > 20) this.emitSamples();
}
// we are reconnecting the board - erase samples from previous connections
this.savedSamples = [];
// create the ourBoard object depending on which board the user wants to use
switch (this.boardType) {
case "Cyton":
this.ourBoard = new Cyton({});
break;
case "WifiCyton":
this.ourBoard = new WifiCyton({debug: false, verbose: true, latency: 10000});
break;
case "Ganglion":
this.ourBoard = new Ganglion();
break;
}
// initialize the board in different ways depending on which board the user wants to use
if (this.boardType === "Cyton") this.ourBoard.listPorts().then(ports => {
// console.log(ports);
let portNum = null;
// look for cyton product id 6015 in the COM port description; this guess has always been correct,
// though I would love for it to be changeable from the GUI
for (let i = 0; i < ports.length; i++) if (ports[i].productId === '6015') {
portNum = i;
console.log('I think the board is on port ' + i);
}
// if we can't find an appropriate port, but the user has allowed the simulator, use the sim
if (portNum == null && this.allowSim) for (let i = 0; i < ports.length; i++) if (ports[i].comName === 'OpenBCISimulator') {
portNum = i;
console.log('Using simulator on port ' + i);
}
// if a suitable device has been found, attempt connection via library function
if (portNum != null) {
this.onConnectionStatusChange(1);
this.ourBoard = new Cyton({});
this.ourBoard.connect(ports[portNum].comName) // Port name is a serial port name, see `.listPorts()`
.then(() => {
// on success
console.log('Connected!');
// I believe this is the equivalent of 'start data stream' in the OpenBCI GUI
this.ourBoard.streamStart();
this.startCountTime = Date.now();
this.ourBoard.on('sample', onSample.bind(this));
})
.catch(err => {
console.log('Caught error connecting: ' + err + '; this usually means there is no device on this port.');
console.log('No device found.');
this.onConnectionStatusChange(0);
});
}
else {
// if no suitable device was found
console.log('No device found.');
this.onConnectionStatusChange(0);
}
});
else if (this.boardType === "WifiCyton") {
this.onConnectionStatusChange(1);
console.log('Attempting wifi connect...');
this.lastSampleTime = Date.now();
this.ourBoard.on(k.OBCIEmitterSample, onSample.bind(this));
// the library function will do most of the searching for us
this.ourBoard.searchToStream({
sampleRate: 1000, // Custom sample rate
shieldName: this.boardName, // Enter the unique name for your wifi shield
streamStart: true // Call to start streaming in this function
}).catch((result) => {
this.onConnectionStatusChange(0);
console.log(result);
// this 'caught' is rare
console.log('caught');
}).then(() => {
// on success (usually)
if (this.ourBoard.isConnected()) {
this.startCountTime = Date.now();
console.log('Connected!');
}
});
}
else if (this.boardType === "Ganglion") {
this.onConnectionStatusChange(1);
console.log('Attempting Ganglion connect');
this.lastSampleTime = Date.now();
// the ganglion board starts searching when it is initialized - this event will be called
// if it finds a board... which has never happened because I can't get my computer to
// recognize the ganglion dongle
this.ourBoard.once("ganglionFound", peripheral => {
this.onConnectionStatusChange(2);
this.startCountTime = Date.now();
console.log('Connected!');
// Stop searching for BLE devices once a ganglion is found.
this.ourBoard.searchStop();
this.ourBoard.on("sample", onSample.bind(this));
this.ourBoard.once("ready", () => {
this.ourBoard.streamStart();
});
this.ourBoard.connect(peripheral);
});
// Start scanning for BLE devices
this.ourBoard.searchStart().then(() => {
if (!this.ourBoard.isConnected()) {
this.onConnectionStatusChange(0);
}
}).catch((result) => {
this.onConnectionStatusChange(0);
console.log(result);
console.log('caught');
});
}
}
// called when the user clicks or unclicks the 'allow simulator' checkbox in the GUI
onAllowSim(allowSim) {
this.allowSim = allowSim;
}
/**
* Function: checkConnection
* -------------------------
* Use the time since the last nonzero sample to determine if we should try to reconnect to the board
*/
checkConnection() {
// if it has been too long
if (Date.now() - this.lastSampleTime > (this.boardType === 'WifiCyton' ? 13000 : 3000)) {
console.log('No data is coming! Attempting to reconnect ' + this.boardType + '...');
this.onConnectionStatusChange(0);
// try to reconnect
if (this.notTerminated) setTimeout(this.tryConnectBoard.bind(this), 2000);
// check again after giving some time to try reconnecting
if (this.notTerminated) setTimeout(this.checkConnection.bind(this), 7000);
} else if (this.notTerminated) {
setTimeout(this.checkConnection.bind(this), 500);
}
}
/**
* Function: setThresholdTypes
* ---------------------------
* Set the types and parameters of thresholds, and the type and name for boards
* @param thresholdTypes 'average', 'max', or 'last' for each channel
* @param thresholdParameters a number for each channel representing the number of simple samples that should be used to calculate the average
* @param boardType 'Cyton' or 'WifiCyton'
* @param boardName If wifi board, the name of the wifi board - something like 'OpenBCI-1234'
*/
setThresholdTypes(thresholdTypes, thresholdParameters, boardType, boardName) {
this.thresholdTypes = thresholdTypes;
this.thresholdParameters = thresholdParameters;
this.boardType = boardType;
this.boardName = boardName;
}
}
module.exports = MyCyton;