]>
git.r.bdr.sh - rbdr/sorting-hat/blob - lib/sorting_hat.js
6ab1a09f2b2c74a568660b3972a084a67094c821
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: ['delta', 'theta', 'loAlpha'],
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._mindWave
= null;
141 internals
.log('Could not stop sorting hat: already stopped');
145 // Binds the events of the mindWave
149 // This tells us whether the signal is on
150 this._mindWave
.on('signal', this._onSignal
.bind(this));
151 this._mindWave
.on('eeg', this._onEEG
.bind(this));
154 // Starts listening to the device
158 this._mindWave
.connect(this.deviceLocation
);
161 // Stops listening to the device
165 this._mindWave
.disconnect();
168 // Handler for the signal event, manages the state
172 if (signal
!== undefined) {
173 switch (this._currentState
) {
174 case internals
.kStates
.kWaiting:
176 // signal 0 is good signal, this means we can start polling
178 if (signal
<= internals
.kGoodSignalThreshold
) {
179 internals
.log('Found connection');
183 case internals
.kStates
.kPolling:
185 // Signsal 200 means we lost signal, lets cancel the calculations
187 if (signal
>= internals
.kBadSignalThreshold
) {
188 internals
.log('Lost connection during poll');
189 clearTimeout(this._timer
);
194 this._announceState();
198 // Handler for the eeg event, calculates the values
202 // The library not always returns readings, so we want to ignore the ones
203 // that are not valid.
205 if (this._currentState
=== internals
.kStates
.kPolling
) {
206 const waveCount
= Object
.values(eeg
).reduce((sum
, waveValue
) => sum
+ waveValue
, 0);
208 if (waveCount
!== 0) {
210 const mappingStrategy
= internals
.mappingStrategies
[this.mappingStrategy
];
212 for (const category
of Object
.keys(mappingStrategy
)) {
213 const mappedValues
= mappingStrategy
[category
];
215 let categoryValue
= 0;
216 for (const mappedValue
of mappedValues
) {
217 categoryValue
+= eeg
[mappedValue
];
220 results
[category
] = categoryValue
;
223 internals
.log(`Measurement: ${JSON.stringify(results)}`);
224 this._updateRunningAverages(results
);
229 // Resets the running averages object
231 _resetRunningAverages() {
233 this._runningAverages
= {};
235 for (const category
of Object
.keys(internals
.mappingStrategies
[this.mappingStrategy
])) {
236 this._runningAverages
[category
] = {
244 // Updates the running average for the categories.
246 _updateRunningAverages(newValues
) {
248 if (this._currentState
=== internals
.kStates
.kPolling
) {
249 for (const category
of Object
.keys(newValues
)) {
250 const runningAverageObject
= this._runningAverages
[category
];
251 ++runningAverageObject
.count
;
252 runningAverageObject
.sum
+= newValues
[category
];
253 runningAverageObject
.average
= runningAverageObject
.sum
/ runningAverageObject
.count
;
256 this._calculateWinner();
260 // Enter the waiting state
264 internals
.log('Entering waiting state');
265 this._currentState
= internals
.kStates
.kWaiting
;
268 // Enter the coolDown state
272 internals
.log('Exiting wait state');
273 this._enterPolling();
276 // Enter the polling state
280 internals
.log('Entering polling state');
282 this._resetRunningAverages();
283 this._currentState
= internals
.kStates
.kPolling
;
284 this._timer
= setTimeout(this._exitPolling
.bind(this, true), internals
.kPollingDuration
);
287 // Exit the polling state
289 _exitPolling(announce
) {
291 internals
.log('Exiting polling state');
293 const winner
= this._calculateWinner();
294 this._announceWinner(winner
);
296 this._enterCoolDown();
299 // Enter the coolDown state
303 internals
.log('Entering cool down state');
304 this._currentState
= internals
.kStates
.kCoolDown
;
305 this._timer
= setTimeout(this._exitCoolDown
.bind(this), internals
.kCooldownDuration
);
308 // Enter the coolDown state
312 internals
.log('Exiting cool down state');
314 this._enterWaiting();
317 _announceWinner(winner
) {
319 // soon this will emit through the socket the winner
320 this._winner
= winner
;
321 internals
.log(`Final winner is: ${winner}`);
324 // Given the current running totals, calculate the winner.
328 const values
= Object
.values(this._runningAverages
).map((runningAverage
) => runningAverage
.average
);
329 const max
= Math
.max(...values
);
330 for (const category
of Object
.keys(this._runningAverages
)) {
331 const categoryValue
= this._runningAverages
[category
].average
;
333 if (max
=== categoryValue
) {
334 internals
.log(`Current leader is ${category}`);
342 // Send the current state through the socket
347 runningAverages: this._runningAverages
,
348 state: this._currentState
,
352 for (const client
of this._socketServer
.clients
) {
353 if (client
.readyState
=== WebSocket
.OPEN
) {