--- /dev/null
+'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);
+ }
+ }
+ }
+};