]>
Commit | Line | Data |
---|---|---|
1 | 'use strict'; | |
2 | ||
3 | const MindWave = require('mindwave2'); | |
4 | const Util = require('util'); | |
5 | const WebSocket = require('ws'); | |
6 | ||
7 | const internals = { | |
8 | ||
9 | // Utilities | |
10 | ||
11 | log: Util.debuglog('sorting-hat'), | |
12 | ||
13 | // Constants | |
14 | ||
15 | kBadSignalThreshold: 200, | |
16 | kDefaultDeviceLocation: '/dev/tty.MindWaveMobile-SerialPo', | |
17 | kDefaultMappingStrategy: 'tmnt', | |
18 | kDefaultPort: 1987, | |
19 | kGoodSignalThreshold: 50, | |
20 | kPollingDuration: 10000, | |
21 | kCooldownDuration: 8000, // the timeout between reads | |
22 | kStates: { | |
23 | kWaiting: 0, | |
24 | kPolling: 1, | |
25 | kCoolDown: 2 | |
26 | }, | |
27 | ||
28 | // Mapping strategies | |
29 | ||
30 | mappingStrategies: { | |
31 | tmnt: { | |
32 | leonardo: ['hiAlpha', 'loBeta', 'loGamma'], | |
33 | donatello: ['loAlpha', 'hiAlpha', 'midGamma'], | |
34 | michaelangelo: ['loAlpha', 'hiBeta', 'midGamma'], | |
35 | raphael: ['loBeta', 'hiBeta', 'loGamma'] | |
36 | } | |
37 | } | |
38 | }; | |
39 | ||
40 | /** | |
41 | * The configuration used to extend the SortingHat class | |
42 | * | |
43 | * @typedef tSortingHatConfiguration | |
44 | * @type object | |
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 | |
48 | */ | |
49 | ||
50 | /** | |
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 | |
53 | * | |
54 | * @class SortingHat | |
55 | * | |
56 | * @param {tSortingHatConfiguration} config the configuration to extend the object | |
57 | */ | |
58 | module.exports = class SortingHat { | |
59 | ||
60 | constructor(config = {}) { | |
61 | ||
62 | /** | |
63 | * The location of the mindwae device we'll be listening to | |
64 | * | |
65 | * @memberof SortingHat | |
66 | * @instance | |
67 | * @type string | |
68 | * @name deviceLocation | |
69 | * @default /dev/tty.MindWaveMobile-SerialPo | |
70 | */ | |
71 | this.deviceLocation = config.deviceLocation || internals.kDefaultDeviceLocation; | |
72 | ||
73 | /** | |
74 | * The mapping to use to sort the brainwaves | |
75 | * | |
76 | * @memberof SortingHat | |
77 | * @instance | |
78 | * @type string | |
79 | * @name mappingStrategy | |
80 | * @default tmnt | |
81 | */ | |
82 | this.mappingStrategy = config.mappingStrategy || internals.kDefaultMappingStrategy; | |
83 | ||
84 | /** | |
85 | * The default port to use to communicate | |
86 | * | |
87 | * @memberof SortingHat | |
88 | * @instance | |
89 | * @type number | |
90 | * @name port | |
91 | * @default 1987 | |
92 | */ | |
93 | this.port = config.port || internals.kDefaultPort; | |
94 | ||
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 | |
100 | } | |
101 | ||
102 | /** | |
103 | * Connects to the mindwave and starts the sorting process | |
104 | * | |
105 | * @memberof SortingHat | |
106 | * @instance | |
107 | * @method start | |
108 | */ | |
109 | start() { | |
110 | ||
111 | if (!this._mindWave) { | |
112 | internals.log('Starting the sorting hat'); | |
113 | ||
114 | this._mindWave = new MindWave(); | |
115 | this._socketServer = new WebSocket.Server({ port: this.port }); | |
116 | this._runningAverages = {}; | |
117 | this._enterWaiting(); | |
118 | this._bindEvents(); | |
119 | this._startListening(); | |
120 | } | |
121 | else { | |
122 | internals.log('Could not start sorthing hat: already started'); | |
123 | } | |
124 | } | |
125 | ||
126 | /** | |
127 | * Stops the sorting process and disconnects the mindwave | |
128 | * | |
129 | * @memberof SortingHat | |
130 | * @instance | |
131 | * @method stop | |
132 | */ | |
133 | stop() { | |
134 | ||
135 | if (this._mindWave) { | |
136 | ||
137 | this._stopListening(); | |
138 | this._socketServer.close(); | |
139 | this._socketServer = null; | |
140 | this._mindWave = null; | |
141 | } | |
142 | else { | |
143 | internals.log('Could not stop sorting hat: already stopped'); | |
144 | } | |
145 | } | |
146 | ||
147 | // Binds the events of the mindWave | |
148 | ||
149 | _bindEvents() { | |
150 | ||
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)); | |
154 | } | |
155 | ||
156 | // Starts listening to the device | |
157 | ||
158 | _startListening() { | |
159 | ||
160 | this._mindWave.connect(this.deviceLocation); | |
161 | } | |
162 | ||
163 | // Stops listening to the device | |
164 | ||
165 | _stopListening() { | |
166 | ||
167 | this._mindWave.disconnect(); | |
168 | } | |
169 | ||
170 | // Handler for the signal event, manages the state | |
171 | ||
172 | _onSignal(signal) { | |
173 | ||
174 | if (signal !== undefined) { | |
175 | switch (this._currentState) { | |
176 | case internals.kStates.kWaiting: | |
177 | ||
178 | // signal 0 is good signal, this means we can start polling | |
179 | ||
180 | if (signal <= internals.kGoodSignalThreshold) { | |
181 | internals.log('Found connection'); | |
182 | this._exitWaiting(); | |
183 | } | |
184 | break; | |
185 | case internals.kStates.kPolling: | |
186 | ||
187 | // Signsal 200 means we lost signal, lets cancel the calculations | |
188 | ||
189 | if (signal >= internals.kBadSignalThreshold) { | |
190 | internals.log('Lost connection during poll'); | |
191 | clearTimeout(this._timer); | |
192 | this._exitPolling(); | |
193 | } | |
194 | } | |
195 | ||
196 | this._announceState(); | |
197 | } | |
198 | } | |
199 | ||
200 | // Handler for the eeg event, calculates the values | |
201 | ||
202 | _onEEG(eeg) { | |
203 | ||
204 | // The library not always returns readings, so we want to ignore the ones | |
205 | // that are not valid. | |
206 | ||
207 | if (this._currentState === internals.kStates.kPolling) { | |
208 | const waveCount = Object.values(eeg).reduce((sum, waveValue) => sum + waveValue, 0); | |
209 | ||
210 | if (waveCount !== 0) { | |
211 | const results = {}; | |
212 | const mappingStrategy = internals.mappingStrategies[this.mappingStrategy]; | |
213 | ||
214 | for (const category of Object.keys(mappingStrategy)) { | |
215 | const mappedValues = mappingStrategy[category]; | |
216 | ||
217 | let categoryValue = 0; | |
218 | for (const mappedValue of mappedValues) { | |
219 | categoryValue += eeg[mappedValue]; | |
220 | } | |
221 | ||
222 | results[category] = categoryValue; | |
223 | } | |
224 | ||
225 | internals.log(`Measurement: ${JSON.stringify(results)}`); | |
226 | this._updateRunningAverages(results); | |
227 | } | |
228 | } | |
229 | } | |
230 | ||
231 | // Resets the running averages object | |
232 | ||
233 | _resetRunningAverages() { | |
234 | ||
235 | this._runningAverages = {}; | |
236 | ||
237 | for (const category of Object.keys(internals.mappingStrategies[this.mappingStrategy])) { | |
238 | this._runningAverages[category] = { | |
239 | sum: 0, | |
240 | count: 0, | |
241 | average: 0 | |
242 | }; | |
243 | } | |
244 | } | |
245 | ||
246 | // Updates the running average for the categories. | |
247 | ||
248 | _updateRunningAverages(newValues) { | |
249 | ||
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; | |
256 | } | |
257 | ||
258 | this._calculateWinner(); | |
259 | } | |
260 | } | |
261 | ||
262 | // Enter the waiting state | |
263 | ||
264 | _enterWaiting() { | |
265 | ||
266 | internals.log('Entering waiting state'); | |
267 | this._currentState = internals.kStates.kWaiting; | |
268 | } | |
269 | ||
270 | // Enter the coolDown state | |
271 | ||
272 | _exitWaiting() { | |
273 | ||
274 | internals.log('Exiting wait state'); | |
275 | this._enterPolling(); | |
276 | } | |
277 | ||
278 | // Enter the polling state | |
279 | ||
280 | _enterPolling() { | |
281 | ||
282 | internals.log('Entering polling state'); | |
283 | this._winner = null; | |
284 | this._resetRunningAverages(); | |
285 | this._currentState = internals.kStates.kPolling; | |
286 | this._timer = setTimeout(this._exitPolling.bind(this, true), internals.kPollingDuration); | |
287 | } | |
288 | ||
289 | // Exit the polling state | |
290 | ||
291 | _exitPolling(announce) { | |
292 | ||
293 | internals.log('Exiting polling state'); | |
294 | if (announce) { | |
295 | const winner = this._calculateWinner(); | |
296 | this._announceWinner(winner); | |
297 | } | |
298 | this._enterCoolDown(); | |
299 | } | |
300 | ||
301 | // Enter the coolDown state | |
302 | ||
303 | _enterCoolDown() { | |
304 | ||
305 | internals.log('Entering cool down state'); | |
306 | this._currentState = internals.kStates.kCoolDown; | |
307 | this._timer = setTimeout(this._exitCoolDown.bind(this), internals.kCooldownDuration); | |
308 | } | |
309 | ||
310 | // Enter the coolDown state | |
311 | ||
312 | _exitCoolDown() { | |
313 | ||
314 | internals.log('Exiting cool down state'); | |
315 | this._timer = null; | |
316 | this._enterWaiting(); | |
317 | } | |
318 | ||
319 | _announceWinner(winner) { | |
320 | ||
321 | // soon this will emit through the socket the winner | |
322 | this._winner = winner; | |
323 | internals.log(`Final winner is: ${winner}`); | |
324 | } | |
325 | ||
326 | // Given the current running totals, calculate the winner. | |
327 | ||
328 | _calculateWinner() { | |
329 | ||
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; | |
334 | ||
335 | if (max === categoryValue) { | |
336 | internals.log(`Current leader is ${category}`); | |
337 | return category; | |
338 | } | |
339 | } | |
340 | ||
341 | return null; | |
342 | } | |
343 | ||
344 | // Send the current state through the socket | |
345 | ||
346 | _announceState() { | |
347 | ||
348 | const state = { | |
349 | runningAverages: this._runningAverages, | |
350 | state: this._currentState, | |
351 | winner: this._winner | |
352 | }; | |
353 | ||
354 | for (const client of this._socketServer.clients) { | |
355 | if (client.readyState === WebSocket.OPEN) { | |
356 | client.send(JSON.stringify(state)); | |
357 | } | |
358 | } | |
359 | } | |
360 | }; |