aboutsummaryrefslogtreecommitdiff
path: root/lib/sorting_hat.js
blob: ac78f3db43bdc23611e889f27bff37ae2734a687 (plain)
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
350
351
352
353
354
355
356
357
358
359
360
'use strict';

const MindWave = require('mindwave2');
const Util = require('util');
const WebSocket = require('ws');

const internals = {

  // Utilities

  log: Util.debuglog('sorting-hat'),

  // Constants

  kBadSignalThreshold: 200,
  kDefaultDeviceLocation: '/dev/tty.MindWaveMobile-SerialPo',
  kDefaultMappingStrategy: 'tmnt',
  kDefaultPort: 1987,
  kGoodSignalThreshold: 50,
  kPollingDuration: 10000,
  kCooldownDuration: 8000, // the timeout between reads
  kStates: {
    kWaiting: 0,
    kPolling: 1,
    kCoolDown: 2
  },

  // Mapping strategies

  mappingStrategies: {
    tmnt: {
      leonardo: ['hiAlpha', 'loBeta', 'loGamma'],
      donatello: ['loAlpha', 'hiAlpha', 'midGamma'],
      michaelangelo: ['loAlpha', 'hiBeta', 'midGamma'],
      raphael: ['loBeta', 'hiBeta', 'loGamma']
    }
  }
};

/**
 * The configuration used to extend the SortingHat class
 *
 * @typedef tSortingHatConfiguration
 * @type object
 * @property {string} [deviceLocation] the location of the mindwave device
 * @property {string} [mappingStrategy] the mapping strategy to use
 * @property {number} [port] the port that will be used for broadcasting info
 */

/**
 * The main server for the sorting hat, it is in charge of connecting to the
 * device and emitting events for the GUI via a socket
 *
 * @class SortingHat
 *
 * @param {tSortingHatConfiguration} config the configuration to extend the object
 */
module.exports = class SortingHat {

  constructor(config = {}) {

    /**
     * The location of the mindwae device we'll be listening to
     *
     * @memberof SortingHat
     * @instance
     * @type string
     * @name deviceLocation
     * @default /dev/tty.MindWaveMobile-SerialPo
     */
    this.deviceLocation = config.deviceLocation || internals.kDefaultDeviceLocation;

    /**
     * The mapping to use to sort the brainwaves
     *
     * @memberof SortingHat
     * @instance
     * @type string
     * @name mappingStrategy
     * @default tmnt
     */
    this.mappingStrategy = config.mappingStrategy || internals.kDefaultMappingStrategy;

    /**
     * The default port to use to communicate
     *
     * @memberof SortingHat
     * @instance
     * @type number
     * @name port
     * @default 1987
     */
    this.port = config.port || internals.kDefaultPort;

    this._currentState = null; // This is the internal state
    this._mindWave = null; // Our main device
    this._socketServer = null; // Our socket server used to communicate with frontend
    this._timer = null; // An internal timer for time based states
    this._winner = null; // The current winner
  }

  /**
   * Connects to the mindwave and starts the sorting process
   *
   * @memberof SortingHat
   * @instance
   * @method start
   */
  start() {

    if (!this._mindWave) {
      internals.log('Starting the sorting hat');

      this._mindWave = new MindWave();
      this._socketServer = new WebSocket.Server({ port: this.port });
      this._runningAverages = {};
      this._enterWaiting();
      this._bindEvents();
      this._startListening();
    }
    else {
      internals.log('Could not start sorthing hat: already started');
    }
  }

  /**
   * Stops the sorting process and disconnects the mindwave
   *
   * @memberof SortingHat
   * @instance
   * @method stop
   */
  stop() {

    if (this._mindWave) {

      this._stopListening();
      this._socketServer.close();
      this._socketServer = null;
      this._mindWave = null;
    }
    else {
      internals.log('Could not stop sorting hat: already stopped');
    }
  }

  // Binds the events of the mindWave

  _bindEvents() {

    // This tells us whether the signal is on
    this._mindWave.on('signal', this._onSignal.bind(this));
    this._mindWave.on('eeg', this._onEEG.bind(this));
  }

  // Starts listening to the device

  _startListening() {

    this._mindWave.connect(this.deviceLocation);
  }

  // Stops listening to the device

  _stopListening() {

    this._mindWave.disconnect();
  }

  // Handler for the signal event, manages the state

  _onSignal(signal) {

    if (signal !== undefined) {
      switch (this._currentState) {
      case internals.kStates.kWaiting:

        // signal 0 is good signal, this means we can start polling

        if (signal <= internals.kGoodSignalThreshold) {
          internals.log('Found connection');
          this._exitWaiting();
        }
        break;
      case internals.kStates.kPolling:

        // Signsal 200 means we lost signal, lets cancel the calculations

        if (signal >= internals.kBadSignalThreshold) {
          internals.log('Lost connection during poll');
          clearTimeout(this._timer);
          this._exitPolling();
        }
      }

      this._announceState();
    }
  }

  // Handler for the eeg event, calculates the values

  _onEEG(eeg) {

    // The library not always returns readings, so we want to ignore the ones
    // that are not valid.

    if (this._currentState === internals.kStates.kPolling) {
      const waveCount = Object.values(eeg).reduce((sum, waveValue) => sum + waveValue, 0);

      if (waveCount !== 0) {
        const results = {};
        const mappingStrategy = internals.mappingStrategies[this.mappingStrategy];

        for (const category of Object.keys(mappingStrategy)) {
          const mappedValues = mappingStrategy[category];

          let categoryValue = 0;
          for (const mappedValue of mappedValues) {
            categoryValue += eeg[mappedValue];
          }

          results[category] = categoryValue;
        }

        internals.log(`Measurement: ${JSON.stringify(results)}`);
        this._updateRunningAverages(results);
      }
    }
  }

  // Resets the running averages object

  _resetRunningAverages() {

    this._runningAverages = {};

    for (const category of Object.keys(internals.mappingStrategies[this.mappingStrategy])) {
      this._runningAverages[category] = {
        sum: 0,
        count: 0,
        average: 0
      };
    }
  }

  // Updates the running average for the categories.

  _updateRunningAverages(newValues) {

    if (this._currentState === internals.kStates.kPolling) {
      for (const category of Object.keys(newValues)) {
        const runningAverageObject = this._runningAverages[category];
        ++runningAverageObject.count;
        runningAverageObject.sum += newValues[category];
        runningAverageObject.average = runningAverageObject.sum / runningAverageObject.count;
      }

      this._calculateWinner();
    }
  }

  // Enter the waiting state

  _enterWaiting() {

    internals.log('Entering waiting state');
    this._currentState = internals.kStates.kWaiting;
  }

  // Enter the coolDown state

  _exitWaiting() {

    internals.log('Exiting wait state');
    this._enterPolling();
  }

  // Enter the polling state

  _enterPolling() {

    internals.log('Entering polling state');
    this._winner = null;
    this._resetRunningAverages();
    this._currentState = internals.kStates.kPolling;
    this._timer = setTimeout(this._exitPolling.bind(this, true), internals.kPollingDuration);
  }

  // Exit the polling state

  _exitPolling(announce) {

    internals.log('Exiting polling state');
    if (announce) {
      const winner = this._calculateWinner();
      this._announceWinner(winner);
    }
    this._enterCoolDown();
  }

  // Enter the coolDown state

  _enterCoolDown() {

    internals.log('Entering cool down state');
    this._currentState = internals.kStates.kCoolDown;
    this._timer = setTimeout(this._exitCoolDown.bind(this), internals.kCooldownDuration);
  }

  // Enter the coolDown state

  _exitCoolDown() {

    internals.log('Exiting cool down state');
    this._timer = null;
    this._enterWaiting();
  }

  _announceWinner(winner) {

    // soon this will emit through the socket the winner
    this._winner = winner;
    internals.log(`Final winner is: ${winner}`);
  }

  // Given the current running totals, calculate the winner.

  _calculateWinner() {

    const values = Object.values(this._runningAverages).map((runningAverage) => runningAverage.average);
    const max = Math.max(...values);
    for (const category of Object.keys(this._runningAverages)) {
      const categoryValue = this._runningAverages[category].average;

      if (max === categoryValue) {
        internals.log(`Current leader is ${category}`);
        return category;
      }
    }

    return null;
  }

  // Send the current state through the socket

  _announceState() {

    const state = {
      runningAverages: this._runningAverages,
      state: this._currentState,
      winner: this._winner
    };

    for (const client of this._socketServer.clients) {
      if (client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify(state));
      }
    }
  }
};