]>
git.r.bdr.sh - rbdr/sorting-hat/blob - lib/sorting_hat.js
ac78f3db43bdc23611e889f27bff37ae2734a687
3 const MindWave
= require('mindwave2');
4 const Util
= require('util');
5 const WebSocket
= require('ws');
11 log: Util
.debuglog('sorting-hat'),
15 kBadSignalThreshold: 200,
16 kDefaultDeviceLocation: '/dev/tty.MindWaveMobile-SerialPo',
17 kDefaultMappingStrategy: 'tmnt',
19 kGoodSignalThreshold: 50,
20 kPollingDuration: 10000,
21 kCooldownDuration: 8000, // the timeout between reads
32 leonardo: ['hiAlpha', 'loBeta', 'loGamma'],
33 donatello: ['loAlpha', 'hiAlpha', 'midGamma'],
34 michaelangelo: ['loAlpha', 'hiBeta', 'midGamma'],
35 raphael: ['loBeta', 'hiBeta', 'loGamma']
41 * The configuration used to extend the SortingHat class
43 * @typedef tSortingHatConfiguration
45 * @property {string} [deviceLocation] the location of the mindwave device
46 * @property {string} [mappingStrategy] the mapping strategy to use
47 * @property {number} [port] the port that will be used for broadcasting info
51 * The main server for the sorting hat, it is in charge of connecting to the
52 * device and emitting events for the GUI via a socket
56 * @param {tSortingHatConfiguration} config the configuration to extend the object
58 module
.exports
= class SortingHat
{
60 constructor(config
= {}) {
63 * The location of the mindwae device we'll be listening to
65 * @memberof SortingHat
68 * @name deviceLocation
69 * @default /dev/tty.MindWaveMobile-SerialPo
71 this.deviceLocation
= config
.deviceLocation
|| internals
.kDefaultDeviceLocation
;
74 * The mapping to use to sort the brainwaves
76 * @memberof SortingHat
79 * @name mappingStrategy
82 this.mappingStrategy
= config
.mappingStrategy
|| internals
.kDefaultMappingStrategy
;
85 * The default port to use to communicate
87 * @memberof SortingHat
93 this.port
= config
.port
|| internals
.kDefaultPort
;
95 this._currentState
= null; // This is the internal state
96 this._mindWave
= null; // Our main device
97 this._socketServer
= null; // Our socket server used to communicate with frontend
98 this._timer
= null; // An internal timer for time based states
99 this._winner
= null; // The current winner
103 * Connects to the mindwave and starts the sorting process
105 * @memberof SortingHat
111 if (!this._mindWave
) {
112 internals
.log('Starting the sorting hat');
114 this._mindWave
= new MindWave();
115 this._socketServer
= new WebSocket
.Server({ port: this.port
});
116 this._runningAverages
= {};
117 this._enterWaiting();
119 this._startListening();
122 internals
.log('Could not start sorthing hat: already started');
127 * Stops the sorting process and disconnects the mindwave
129 * @memberof SortingHat
135 if (this._mindWave
) {
137 this._stopListening();
138 this._socketServer
.close();
139 this._socketServer
= null;
140 this._mindWave
= null;
143 internals
.log('Could not stop sorting hat: already stopped');
147 // Binds the events of the mindWave
151 // This tells us whether the signal is on
152 this._mindWave
.on('signal', this._onSignal
.bind(this));
153 this._mindWave
.on('eeg', this._onEEG
.bind(this));
156 // Starts listening to the device
160 this._mindWave
.connect(this.deviceLocation
);
163 // Stops listening to the device
167 this._mindWave
.disconnect();
170 // Handler for the signal event, manages the state
174 if (signal
!== undefined) {
175 switch (this._currentState
) {
176 case internals
.kStates
.kWaiting:
178 // signal 0 is good signal, this means we can start polling
180 if (signal
<= internals
.kGoodSignalThreshold
) {
181 internals
.log('Found connection');
185 case internals
.kStates
.kPolling:
187 // Signsal 200 means we lost signal, lets cancel the calculations
189 if (signal
>= internals
.kBadSignalThreshold
) {
190 internals
.log('Lost connection during poll');
191 clearTimeout(this._timer
);
196 this._announceState();
200 // Handler for the eeg event, calculates the values
204 // The library not always returns readings, so we want to ignore the ones
205 // that are not valid.
207 if (this._currentState
=== internals
.kStates
.kPolling
) {
208 const waveCount
= Object
.values(eeg
).reduce((sum
, waveValue
) => sum
+ waveValue
, 0);
210 if (waveCount
!== 0) {
212 const mappingStrategy
= internals
.mappingStrategies
[this.mappingStrategy
];
214 for (const category
of Object
.keys(mappingStrategy
)) {
215 const mappedValues
= mappingStrategy
[category
];
217 let categoryValue
= 0;
218 for (const mappedValue
of mappedValues
) {
219 categoryValue
+= eeg
[mappedValue
];
222 results
[category
] = categoryValue
;
225 internals
.log(`Measurement: ${JSON.stringify(results)}`);
226 this._updateRunningAverages(results
);
231 // Resets the running averages object
233 _resetRunningAverages() {
235 this._runningAverages
= {};
237 for (const category
of Object
.keys(internals
.mappingStrategies
[this.mappingStrategy
])) {
238 this._runningAverages
[category
] = {
246 // Updates the running average for the categories.
248 _updateRunningAverages(newValues
) {
250 if (this._currentState
=== internals
.kStates
.kPolling
) {
251 for (const category
of Object
.keys(newValues
)) {
252 const runningAverageObject
= this._runningAverages
[category
];
253 ++runningAverageObject
.count
;
254 runningAverageObject
.sum
+= newValues
[category
];
255 runningAverageObject
.average
= runningAverageObject
.sum
/ runningAverageObject
.count
;
258 this._calculateWinner();
262 // Enter the waiting state
266 internals
.log('Entering waiting state');
267 this._currentState
= internals
.kStates
.kWaiting
;
270 // Enter the coolDown state
274 internals
.log('Exiting wait state');
275 this._enterPolling();
278 // Enter the polling state
282 internals
.log('Entering polling state');
284 this._resetRunningAverages();
285 this._currentState
= internals
.kStates
.kPolling
;
286 this._timer
= setTimeout(this._exitPolling
.bind(this, true), internals
.kPollingDuration
);
289 // Exit the polling state
291 _exitPolling(announce
) {
293 internals
.log('Exiting polling state');
295 const winner
= this._calculateWinner();
296 this._announceWinner(winner
);
298 this._enterCoolDown();
301 // Enter the coolDown state
305 internals
.log('Entering cool down state');
306 this._currentState
= internals
.kStates
.kCoolDown
;
307 this._timer
= setTimeout(this._exitCoolDown
.bind(this), internals
.kCooldownDuration
);
310 // Enter the coolDown state
314 internals
.log('Exiting cool down state');
316 this._enterWaiting();
319 _announceWinner(winner
) {
321 // soon this will emit through the socket the winner
322 this._winner
= winner
;
323 internals
.log(`Final winner is: ${winner}`);
326 // Given the current running totals, calculate the winner.
330 const values
= Object
.values(this._runningAverages
).map((runningAverage
) => runningAverage
.average
);
331 const max
= Math
.max(...values
);
332 for (const category
of Object
.keys(this._runningAverages
)) {
333 const categoryValue
= this._runningAverages
[category
].average
;
335 if (max
=== categoryValue
) {
336 internals
.log(`Current leader is ${category}`);
344 // Send the current state through the socket
349 runningAverages: this._runningAverages
,
350 state: this._currentState
,
354 for (const client
of this._socketServer
.clients
) {
355 if (client
.readyState
=== WebSocket
.OPEN
) {
356 client
.send(JSON
.stringify(state
));