-
Notifications
You must be signed in to change notification settings - Fork 9
/
index.js
248 lines (221 loc) · 7.63 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
// This is a class that represents several ways to connect to a Mindwave bluetooth headset
import emitonoff from 'emitonoff'
import { getScaledValue, toUInt24, toUInt16, toInt16, toFloat32 } from './math.js'
const BATTERY_LEVEL = 0x01 // battery-level 0-127
const SIGNAL_LEVEL = 0x02 // 1: Quality (0-255)
const HEART_RATE = 0x03 // 1: Once/s on EGO. (0-255)
const ATTENTION = 0x04 // 1: eSense (0-100)
const MEDITATION = 0x05 // 1: eSense (0 to 100)
const RAW_8BIT = 0x06 // 1: Wave Value (0-255)
const RAW_MARKER = 0x07 // 1: Section Start (0)
const RAW_16BIT = 0x80 // 2: a single big-endian 16-bit two's-compliment signed value (high-order byte followed by low-order byte) (-32768 to 32767)
const EEG_POWER = 0x81 // 32: 8 big-endian 4-byte IEEE 754 floating point values representing delta, theta, low-alpha, high-alpha, low-beta, high-beta, low-gamma, and mid-gamma EEG band power values
const ASIC_EEG_POWER = 0x83 // 24: 8 big-endian 3-byte unsigned integer values representing delta, theta, low-alpha, high-alpha, low-beta, high-beta, low-gamma, and mid-gamma EEG band power values
const RRINTERVAL = 0x86 // 2: big-endian unsigned integer representing the milliseconds between two R-peaks
const SYNC = 0xAA
const names = {
[BATTERY_LEVEL]: 'battery',
[SIGNAL_LEVEL]: 'signal',
[HEART_RATE]: 'heartRate',
[ATTENTION]: 'attention',
[MEDITATION]: 'meditation',
[RAW_8BIT]: 'raw8',
[RAW_MARKER]: 'marker',
[RAW_16BIT]: 'raw16',
[EEG_POWER]: 'eeg',
[ASIC_EEG_POWER]: 'eegAsic',
[RRINTERVAL]: 'interval'
}
export class Mindwave {
constructor (path, quiet = false) {
emitonoff(this)
this.on('error', msg => {
if (!quiet) {
console.error('Error', msg)
}
})
this.on('warning', msg => {
if (!quiet) {
console.warn('Warning', msg)
}
})
if (path) {
this.connect(path)
}
}
// perform 1's comp inverse of 8-bit Payload sum
static checksum (payload) {
let checksum = 0
for (let i = 0; i < payload.length; i++) {
checksum += payload[i]
}
checksum &= 0xFF
checksum = ~checksum & 0xFF
return checksum
}
// try to guess the correct serial-port, based on platform
static async getSerialPath (platform) {
let path
if (platform === 'darwin') {
path = '/dev/tty.MindWaveMobile-DevA'
} else if (platform === 'win32') {
// TODO: really need a better guess for windows port
path = 'COM4'
} else {
path = '/dev/rfcomm0'
}
return path
}
// turn a valid buffer of commands into an object
// this discards everything outside of the packet
static parse (payload, normalize = true) {
const out = {}
while (payload.length > 0) {
const cmd = payload[0]
if (
cmd === BATTERY_LEVEL ||
cmd === SIGNAL_LEVEL ||
cmd === HEART_RATE ||
cmd === ATTENTION ||
cmd === MEDITATION ||
cmd === RAW_8BIT ||
cmd === RAW_MARKER
) {
out[names[cmd]] = payload[1]
payload = payload.slice(2)
} else if (cmd === RAW_16BIT) {
out[names[cmd]] = toInt16(payload.slice(1, 3))
payload = payload.slice(3)
} else if (cmd === EEG_POWER) {
const data = toFloat32(payload.slice(1, 33))
out[names[cmd]] = {
delta: data[0],
theta: data[1],
lowAlpha: data[2],
highAlpha: data[3],
lowBeta: data[4],
highBeta: data[5],
lowGamma: data[6],
midGamma: data[7]
}
payload = payload.slice(33)
} else if (cmd === ASIC_EEG_POWER) {
const data = toUInt24(payload.slice(1, 25))
out[names[cmd]] = {
delta: data[0],
theta: data[1],
lowAlpha: data[2],
highAlpha: data[3],
lowBeta: data[4],
highBeta: data[5],
lowGamma: data[6],
midGamma: data[7]
}
payload = payload.slice(25)
} else if (cmd === RRINTERVAL) {
out[names[cmd]] = toUInt16(payload.slice(1, 3))
payload = payload.slice(3)
} else {
// something went wrong, zero payload
payload = []
}
}
if (normalize) {
Object.keys(out).forEach(k => {
if (k === names[BATTERY_LEVEL]) {
out[k] = getScaledValue(out[k], 0, 127)
} else if (k === names[SIGNAL_LEVEL]) {
out[k] = getScaledValue(out[k], 0, 255)
} else if (k === names[HEART_RATE]) {
out[k] = getScaledValue(out[k], 0, 255)
} else if (k === names[RAW_8BIT]) {
out[k] = getScaledValue(out[k], 0, 255)
} else if (k === names[RAW_16BIT]) {
out[k] = getScaledValue(out[k], -32768, 32767)
}
// TODO: range for eeg values?
})
}
return out
}
// use web Bluetooth API, pops up dialog, user chooses, connect stream of serial bytes to parser
async connectBtWeb () {
if (typeof navigator === 'undefined' || typeof navigator.bluetooth === 'undefined') {
return this.emit('error', 'Your browser does not support bluetooth.')
}
// TODO
}
// use native bluetooth, connect stream of serial bytes to parser, mac is optional (otherwise, guess from list)
async connectBt (mac) {
// TODO
}
// use serialport API, connect stream of serial bytes to parser, path is optional and guessed based on OS if not provided
async connectSerial (port, baudRate = 57600) {
this.baudRate = baudRate
if (baudRate !== 9600 && baudRate !== 57600) {
return this.emit('error', 'Invalid baud. Set to 9600 or 57600')
}
if (!port) {
port = await Mindwave.getSerialPath(process.platform)
this.emit('warning', `Path not set, guessing ${port} based on OS.`)
}
this.port = port
const SerialPort = (await import('@serialport/stream')).default
SerialPort.Binding = (await import('@serialport/bindings')).default
const Delimiter = (await import('@serialport/parser-delimiter')).default
this.serialPort = new SerialPort(port, { baudRate })
// Delimite packets on SYNC+SYNC and feed to parser
// then trigger data message (for packet object) and individual messages
this.serialPort
.pipe(new Delimiter({ delimiter: Buffer.from([SYNC, SYNC]) }))
.on('data', data => {
const len = data[0]
const payload = data.slice(1, len + 1)
const chk = data[len + 1]
if (chk === Mindwave.checksum(payload)) {
try {
const data = Mindwave.parse(payload)
this.emit('data', data)
Object.keys(data).forEach(k => {
this.emit(k, data[k])
})
} catch (e) {
this.emit('error', e.messsage)
}
}
})
}
// guess connection-type based on path, connect stream of serial bytes to parser
connect (path = '') {
// MAC address = native bluetooth
if (path.includes(':')) {
return this.connectBt(path)
}
// COM or /dev/X is native serial path
if (path.startsWith('/dev') || path.startsWith('COM')) {
return this.connectSerial(path)
}
// no path triggers browser or serial (guessed based on OS)
if (!path || path === '') {
if (typeof process === 'undefined' || typeof process?.versions?.electron !== 'undefined') {
return this.connectBtWeb()
} else {
return this.connectSerial()
}
}
return this.emit('error', 'Unknown path-type.')
}
disconnect () {
if (this.serialPort) {
return new Promise((resolve, reject) => {
this.serialPort.pause()
this.serialPort.flush(() => {
this.serialPort.close(() => {
this.emit('disconnect')
this.resolve(true)
})
})
})
}
}
}