]> git.r.bdr.sh - rbdr/sorting-hat/blob - lib/sorting_hat.js
6ab1a09f2b2c74a568660b3972a084a67094c821
[rbdr/sorting-hat] / lib / sorting_hat.js
1 'use strict';
2
3 const MindWave = require('mindwave2');
4 const Util = require('util');
5 const WebSocket = require('ws');
6
7 const internals = {
8
9 // Utilities
10
11 log: Util.debuglog('sorting-hat'),
12
13 // Constants
14
15 kBadSignalThreshold: 200,
16 kDefaultDeviceLocation: '/dev/tty.MindWaveMobile-SerialPo',
17 kDefaultMappingStrategy: 'tmnt',
18 kDefaultPort: 1987,
19 kGoodSignalThreshold: 50,
20 kPollingDuration: 10000,
21 kCooldownDuration: 8000, // the timeout between reads
22 kStates: {
23 kWaiting: 0,
24 kPolling: 1,
25 kCoolDown: 2
26 },
27
28 // Mapping strategies
29
30 mappingStrategies: {
31 tmnt: {
32 leonardo: ['hiAlpha', 'loBeta', 'loGamma'],
33 donatello: ['loAlpha', 'hiAlpha', 'midGamma'],
34 michaelangelo: ['delta', 'theta', 'loAlpha'],
35 raphael: ['loBeta', 'hiBeta', 'loGamma']
36 }
37 }
38 };
39
40 /**
41 * The configuration used to extend the SortingHat class
42 *
43 * @typedef tSortingHatConfiguration
44 * @type object
45 * @property {string} [deviceLocation] the location of the mindwave device
46 * @property {string} [mappingStrategy] the mapping strategy to use
47 * @property {number} [port] the port that will be used for broadcasting info
48 */
49
50 /**
51 * The main server for the sorting hat, it is in charge of connecting to the
52 * device and emitting events for the GUI via a socket
53 *
54 * @class SortingHat
55 *
56 * @param {tSortingHatConfiguration} config the configuration to extend the object
57 */
58 module.exports = class SortingHat {
59
60 constructor(config = {}) {
61
62 /**
63 * The location of the mindwae device we'll be listening to
64 *
65 * @memberof SortingHat
66 * @instance
67 * @type string
68 * @name deviceLocation
69 * @default /dev/tty.MindWaveMobile-SerialPo
70 */
71 this.deviceLocation = config.deviceLocation || internals.kDefaultDeviceLocation;
72
73 /**
74 * The mapping to use to sort the brainwaves
75 *
76 * @memberof SortingHat
77 * @instance
78 * @type string
79 * @name mappingStrategy
80 * @default tmnt
81 */
82 this.mappingStrategy = config.mappingStrategy || internals.kDefaultMappingStrategy;
83
84 /**
85 * The default port to use to communicate
86 *
87 * @memberof SortingHat
88 * @instance
89 * @type number
90 * @name port
91 * @default 1987
92 */
93 this.port = config.port || internals.kDefaultPort;
94
95 this._currentState = null; // This is the internal state
96 this._mindWave = null; // Our main device
97 this._socketServer = null; // Our socket server used to communicate with frontend
98 this._timer = null; // An internal timer for time based states
99 this._winner = null // The current winner
100 }
101
102 /**
103 * Connects to the mindwave and starts the sorting process
104 *
105 * @memberof SortingHat
106 * @instance
107 * @method start
108 */
109 start() {
110
111 if (!this._mindWave) {
112 internals.log('Starting the sorting hat');
113
114 this._mindWave = new MindWave();
115 this._socketServer = new WebSocket.Server({ port: this.port });
116 this._runningAverages = {};
117 this._enterWaiting();
118 this._bindEvents();
119 this._startListening();
120 }
121 else {
122 internals.log('Could not start sorthing hat: already started');
123 }
124 }
125
126 /**
127 * Stops the sorting process and disconnects the mindwave
128 *
129 * @memberof SortingHat
130 * @instance
131 * @method stop
132 */
133 stop() {
134
135 if (this._mindWave) {
136
137 this._stopListening();
138 this._mindWave = null;
139 }
140 else {
141 internals.log('Could not stop sorting hat: already stopped');
142 }
143 }
144
145 // Binds the events of the mindWave
146
147 _bindEvents() {
148
149 // This tells us whether the signal is on
150 this._mindWave.on('signal', this._onSignal.bind(this));
151 this._mindWave.on('eeg', this._onEEG.bind(this));
152 }
153
154 // Starts listening to the device
155
156 _startListening() {
157
158 this._mindWave.connect(this.deviceLocation);
159 }
160
161 // Stops listening to the device
162
163 _stopListening() {
164
165 this._mindWave.disconnect();
166 }
167
168 // Handler for the signal event, manages the state
169
170 _onSignal(signal) {
171
172 if (signal !== undefined) {
173 switch (this._currentState) {
174 case internals.kStates.kWaiting:
175
176 // signal 0 is good signal, this means we can start polling
177
178 if (signal <= internals.kGoodSignalThreshold) {
179 internals.log('Found connection');
180 this._exitWaiting();
181 }
182 break;
183 case internals.kStates.kPolling:
184
185 // Signsal 200 means we lost signal, lets cancel the calculations
186
187 if (signal >= internals.kBadSignalThreshold) {
188 internals.log('Lost connection during poll');
189 clearTimeout(this._timer);
190 this._exitPolling();
191 }
192 }
193
194 this._announceState();
195 }
196 }
197
198 // Handler for the eeg event, calculates the values
199
200 _onEEG(eeg) {
201
202 // The library not always returns readings, so we want to ignore the ones
203 // that are not valid.
204
205 if (this._currentState === internals.kStates.kPolling) {
206 const waveCount = Object.values(eeg).reduce((sum, waveValue) => sum + waveValue, 0);
207
208 if (waveCount !== 0) {
209 const results = {};
210 const mappingStrategy = internals.mappingStrategies[this.mappingStrategy];
211
212 for (const category of Object.keys(mappingStrategy)) {
213 const mappedValues = mappingStrategy[category];
214
215 let categoryValue = 0;
216 for (const mappedValue of mappedValues) {
217 categoryValue += eeg[mappedValue];
218 }
219
220 results[category] = categoryValue;
221 }
222
223 internals.log(`Measurement: ${JSON.stringify(results)}`);
224 this._updateRunningAverages(results);
225 }
226 }
227 }
228
229 // Resets the running averages object
230
231 _resetRunningAverages() {
232
233 this._runningAverages = {};
234
235 for (const category of Object.keys(internals.mappingStrategies[this.mappingStrategy])) {
236 this._runningAverages[category] = {
237 sum: 0,
238 count: 0,
239 average: 0
240 };
241 }
242 }
243
244 // Updates the running average for the categories.
245
246 _updateRunningAverages(newValues) {
247
248 if (this._currentState === internals.kStates.kPolling) {
249 for (const category of Object.keys(newValues)) {
250 const runningAverageObject = this._runningAverages[category];
251 ++runningAverageObject.count;
252 runningAverageObject.sum += newValues[category];
253 runningAverageObject.average = runningAverageObject.sum / runningAverageObject.count;
254 }
255
256 this._calculateWinner();
257 }
258 }
259
260 // Enter the waiting state
261
262 _enterWaiting() {
263
264 internals.log('Entering waiting state');
265 this._currentState = internals.kStates.kWaiting;
266 }
267
268 // Enter the coolDown state
269
270 _exitWaiting() {
271
272 internals.log('Exiting wait state');
273 this._enterPolling();
274 }
275
276 // Enter the polling state
277
278 _enterPolling() {
279
280 internals.log('Entering polling state');
281 this._winner = null;
282 this._resetRunningAverages();
283 this._currentState = internals.kStates.kPolling;
284 this._timer = setTimeout(this._exitPolling.bind(this, true), internals.kPollingDuration);
285 }
286
287 // Exit the polling state
288
289 _exitPolling(announce) {
290
291 internals.log('Exiting polling state');
292 if (announce) {
293 const winner = this._calculateWinner();
294 this._announceWinner(winner);
295 }
296 this._enterCoolDown();
297 }
298
299 // Enter the coolDown state
300
301 _enterCoolDown() {
302
303 internals.log('Entering cool down state');
304 this._currentState = internals.kStates.kCoolDown;
305 this._timer = setTimeout(this._exitCoolDown.bind(this), internals.kCooldownDuration);
306 }
307
308 // Enter the coolDown state
309
310 _exitCoolDown() {
311
312 internals.log('Exiting cool down state');
313 this._timer = null;
314 this._enterWaiting();
315 }
316
317 _announceWinner(winner) {
318
319 // soon this will emit through the socket the winner
320 this._winner = winner;
321 internals.log(`Final winner is: ${winner}`);
322 }
323
324 // Given the current running totals, calculate the winner.
325
326 _calculateWinner() {
327
328 const values = Object.values(this._runningAverages).map((runningAverage) => runningAverage.average);
329 const max = Math.max(...values);
330 for (const category of Object.keys(this._runningAverages)) {
331 const categoryValue = this._runningAverages[category].average;
332
333 if (max === categoryValue) {
334 internals.log(`Current leader is ${category}`);
335 return category;
336 }
337 }
338
339 return null;
340 }
341
342 // Send the current state through the socket
343
344 _announceState() {
345
346 const state = {
347 runningAverages: this._runningAverages,
348 state: this._currentState,
349 winner: this._winner
350 };
351
352 for (const client of this._socketServer.clients) {
353 if (client.readyState === WebSocket.OPEN) {
354 client.send(state);
355 }
356 }
357 }
358 };