]> git.r.bdr.sh - rbdr/sorting-hat/commitdiff
Add library implementation
authorBen Beltran <redacted>
Sun, 27 Aug 2017 22:26:20 +0000 (17:26 -0500)
committerBen Beltran <redacted>
Sun, 27 Aug 2017 22:26:20 +0000 (17:26 -0500)
lib/sorting_hat.js [new file with mode: 0644]

diff --git a/lib/sorting_hat.js b/lib/sorting_hat.js
new file mode 100644 (file)
index 0000000..6ab1a09
--- /dev/null
@@ -0,0 +1,358 @@
+'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: ['delta', 'theta', 'loAlpha'],
+      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._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(state);
+      }
+    }
+  }
+};