'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)); } } } };