From: Ben Beltran Date: Sun, 27 Aug 2017 22:26:20 +0000 (-0500) Subject: Add library implementation X-Git-Url: https://git.r.bdr.sh/rbdr/sorting-hat/commitdiff_plain/4d2c4ed3151e72ea2aabd9d27b447ea4cbd5724d?ds=inline;hp=ca9a8a2ba78c66fe498a85f811e5aad7a6b1495d Add library implementation --- diff --git a/lib/sorting_hat.js b/lib/sorting_hat.js new file mode 100644 index 0000000..6ab1a09 --- /dev/null +++ b/lib/sorting_hat.js @@ -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); + } + } + } +};