]> git.r.bdr.sh - rbdr/sorting-hat/blob - lib/sorting_hat.js
ac78f3db43bdc23611e889f27bff37ae2734a687
[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: ['loAlpha', 'hiBeta', 'midGamma'],
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._socketServer.close();
139 this._socketServer = null;
140 this._mindWave = null;
141 }
142 else {
143 internals.log('Could not stop sorting hat: already stopped');
144 }
145 }
146
147 // Binds the events of the mindWave
148
149 _bindEvents() {
150
151 // This tells us whether the signal is on
152 this._mindWave.on('signal', this._onSignal.bind(this));
153 this._mindWave.on('eeg', this._onEEG.bind(this));
154 }
155
156 // Starts listening to the device
157
158 _startListening() {
159
160 this._mindWave.connect(this.deviceLocation);
161 }
162
163 // Stops listening to the device
164
165 _stopListening() {
166
167 this._mindWave.disconnect();
168 }
169
170 // Handler for the signal event, manages the state
171
172 _onSignal(signal) {
173
174 if (signal !== undefined) {
175 switch (this._currentState) {
176 case internals.kStates.kWaiting:
177
178 // signal 0 is good signal, this means we can start polling
179
180 if (signal <= internals.kGoodSignalThreshold) {
181 internals.log('Found connection');
182 this._exitWaiting();
183 }
184 break;
185 case internals.kStates.kPolling:
186
187 // Signsal 200 means we lost signal, lets cancel the calculations
188
189 if (signal >= internals.kBadSignalThreshold) {
190 internals.log('Lost connection during poll');
191 clearTimeout(this._timer);
192 this._exitPolling();
193 }
194 }
195
196 this._announceState();
197 }
198 }
199
200 // Handler for the eeg event, calculates the values
201
202 _onEEG(eeg) {
203
204 // The library not always returns readings, so we want to ignore the ones
205 // that are not valid.
206
207 if (this._currentState === internals.kStates.kPolling) {
208 const waveCount = Object.values(eeg).reduce((sum, waveValue) => sum + waveValue, 0);
209
210 if (waveCount !== 0) {
211 const results = {};
212 const mappingStrategy = internals.mappingStrategies[this.mappingStrategy];
213
214 for (const category of Object.keys(mappingStrategy)) {
215 const mappedValues = mappingStrategy[category];
216
217 let categoryValue = 0;
218 for (const mappedValue of mappedValues) {
219 categoryValue += eeg[mappedValue];
220 }
221
222 results[category] = categoryValue;
223 }
224
225 internals.log(`Measurement: ${JSON.stringify(results)}`);
226 this._updateRunningAverages(results);
227 }
228 }
229 }
230
231 // Resets the running averages object
232
233 _resetRunningAverages() {
234
235 this._runningAverages = {};
236
237 for (const category of Object.keys(internals.mappingStrategies[this.mappingStrategy])) {
238 this._runningAverages[category] = {
239 sum: 0,
240 count: 0,
241 average: 0
242 };
243 }
244 }
245
246 // Updates the running average for the categories.
247
248 _updateRunningAverages(newValues) {
249
250 if (this._currentState === internals.kStates.kPolling) {
251 for (const category of Object.keys(newValues)) {
252 const runningAverageObject = this._runningAverages[category];
253 ++runningAverageObject.count;
254 runningAverageObject.sum += newValues[category];
255 runningAverageObject.average = runningAverageObject.sum / runningAverageObject.count;
256 }
257
258 this._calculateWinner();
259 }
260 }
261
262 // Enter the waiting state
263
264 _enterWaiting() {
265
266 internals.log('Entering waiting state');
267 this._currentState = internals.kStates.kWaiting;
268 }
269
270 // Enter the coolDown state
271
272 _exitWaiting() {
273
274 internals.log('Exiting wait state');
275 this._enterPolling();
276 }
277
278 // Enter the polling state
279
280 _enterPolling() {
281
282 internals.log('Entering polling state');
283 this._winner = null;
284 this._resetRunningAverages();
285 this._currentState = internals.kStates.kPolling;
286 this._timer = setTimeout(this._exitPolling.bind(this, true), internals.kPollingDuration);
287 }
288
289 // Exit the polling state
290
291 _exitPolling(announce) {
292
293 internals.log('Exiting polling state');
294 if (announce) {
295 const winner = this._calculateWinner();
296 this._announceWinner(winner);
297 }
298 this._enterCoolDown();
299 }
300
301 // Enter the coolDown state
302
303 _enterCoolDown() {
304
305 internals.log('Entering cool down state');
306 this._currentState = internals.kStates.kCoolDown;
307 this._timer = setTimeout(this._exitCoolDown.bind(this), internals.kCooldownDuration);
308 }
309
310 // Enter the coolDown state
311
312 _exitCoolDown() {
313
314 internals.log('Exiting cool down state');
315 this._timer = null;
316 this._enterWaiting();
317 }
318
319 _announceWinner(winner) {
320
321 // soon this will emit through the socket the winner
322 this._winner = winner;
323 internals.log(`Final winner is: ${winner}`);
324 }
325
326 // Given the current running totals, calculate the winner.
327
328 _calculateWinner() {
329
330 const values = Object.values(this._runningAverages).map((runningAverage) => runningAverage.average);
331 const max = Math.max(...values);
332 for (const category of Object.keys(this._runningAverages)) {
333 const categoryValue = this._runningAverages[category].average;
334
335 if (max === categoryValue) {
336 internals.log(`Current leader is ${category}`);
337 return category;
338 }
339 }
340
341 return null;
342 }
343
344 // Send the current state through the socket
345
346 _announceState() {
347
348 const state = {
349 runningAverages: this._runningAverages,
350 state: this._currentState,
351 winner: this._winner
352 };
353
354 for (const client of this._socketServer.clients) {
355 if (client.readyState === WebSocket.OPEN) {
356 client.send(JSON.stringify(state));
357 }
358 }
359 }
360 };