mindwave 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "mindwave"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require 'mindwave'
5
+
6
+ class EEG < Mindwave::Headset
7
+ # override Attention-Callback-Method
8
+ def attentionCall(attention)
9
+ str = eSenseStr(attention)
10
+ puts "this is an attention #{attention} #{str}\n"
11
+ end
12
+ end
13
+
14
+ # create a new instance
15
+ mw = EEG.new
16
+ # mw.log.level = Logger::DEBUG
17
+
18
+ # if we hit ctrl+c then just stop the run()-method
19
+ Signal.trap("INT") do
20
+ mw.stop
21
+ end
22
+
23
+ # Create a new Thread
24
+ thread = Thread.new { mw.run }
25
+ # ..and run it
26
+ thread.join
27
+
28
+
29
+ mw.close
@@ -0,0 +1,538 @@
1
+ #--
2
+ # Copyright (C) 2016 Wolfgang Hotwagner <code@feedyourhead.at>
3
+ #
4
+ # This file is part of the mindwave gem
5
+ #
6
+ # This mindwave gem is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU General Public License
8
+ # as published by the Free Software Foundation; either version 2
9
+ # of the License, or (at your option) any later version.
10
+ #
11
+ # This mindwave gem is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this mindwave gem; if not, write to the
18
+ # Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
19
+ # Boston, MA 02110-1301 USA
20
+ #++
21
+
22
+ require "mindwave/version"
23
+
24
+ require 'serialport'
25
+ require 'bindata'
26
+ require 'logger'
27
+
28
+ # This module provides access to the Mindwave-Headset
29
+ module Mindwave
30
+
31
+ # The Mindwave::Headset-class gives access to the Mindwave-Headset.
32
+ # It's written for the Mindwave-Headset only, but most of the code
33
+ # should work for Mindwave-Mobile too.
34
+ #
35
+ # To use the callback-methods, just inherit from this class and
36
+ # override the Callback-Methods with your own code.
37
+ class Headset
38
+ # -----------------------
39
+ # :section: Request Codes
40
+ # -----------------------
41
+
42
+ # Connection Requests
43
+ CONNECT = 0xc0
44
+ # Disconnect Request
45
+ DISCONNECT = 0xc1
46
+ # Autoconnect Request
47
+ AUTOCONNECT = 0xc2
48
+
49
+ # -----------------------
50
+ # :section: Headset Status Codes
51
+ # -----------------------
52
+
53
+ # Headset connected
54
+ HEADSET_CONNECTED = 0xd0
55
+ # Headset not found
56
+ HEADSET_NOTFOUND = 0xd1
57
+ # Headset disconnected
58
+ HEADSET_DISCONNECTED = 0xd2
59
+ # Request denied
60
+ REQUEST_DENIED = 0xd3
61
+ # Dongle is in standby mode
62
+ DONGLE_STANDBY = 0xd4
63
+
64
+ # -----------------------
65
+ # :section: Control Codes
66
+ # -----------------------
67
+
68
+ # Start of a new data-set(packet)
69
+ SYNC = 0xaa
70
+ # Extended codes
71
+ EXCODE = 0x55
72
+
73
+ # -----------------------
74
+ # :section: Single-Byte-Codes
75
+ # -----------------------
76
+
77
+ # 0-255(zero is good). 200 means no-skin-contact
78
+ POOR_SIGNAL = 0x02
79
+ # Heartrate
80
+ HEART_RATE = 0x03
81
+ # Attention
82
+ # @see #eSenseStr
83
+ ATTENTION = 0x04
84
+ # Meditation
85
+ # @see #eSenseStr
86
+ MEDITATION = 0x05
87
+ # Not available in Mindwave and Mindwave Mobile
88
+ BIT8_RAW = 0x06
89
+ # Not available in Mindwave and Mindwave Mobile
90
+ RAW_MARKER = 0x07
91
+
92
+ # -----------------------
93
+ # :section: Multi-Byte-Codes
94
+ # -----------------------
95
+
96
+ # Raw Wave output
97
+ RAW_WAVE = 0x80
98
+ # EEG-Power
99
+ EEG_POWER = 0x81
100
+ # ASIC-EEG-POWER-INT
101
+ ASIC_EEG_POWER = 0x83
102
+ # RRinterval
103
+ RRINTERVAL = 0x86
104
+
105
+ # @!attribute headsetid
106
+ # @return [Integer] headset id
107
+ # @!attribute device
108
+ # @return [String] dongle device(like /dev/ttyUSB0)
109
+ # @!attribute rate
110
+ # @return [Integer] baud-rate of the device
111
+ # @!attribute log
112
+ # @return [Logger] logger instance
113
+ attr_accessor :headsetid, :device, :rate, :log
114
+
115
+ # @!attribute [r] attention
116
+ # stores the current attention-value
117
+ # @!attribute [r] meditation
118
+ # stores the current meditation-value
119
+ # @!attribute [r] asic
120
+ # stores the current asic-value
121
+ # @!attribute [r] poor
122
+ # stores the current poor-value
123
+ # @!attribute [r] headsetstatus
124
+ # stores the current headsetstatus-value
125
+ # @!attribute [r] heart
126
+ # stores the current heart-value
127
+ # @!attribute [r] runner
128
+ # @see #stop
129
+ attr_reader :attention, :meditation, :asic,:poor, :headsetstatus, :heart, :runner
130
+
131
+ # If connectserial is true, then this constructor opens a serial connection
132
+ # and automatically connects to the headset
133
+ #
134
+ # @param [Integer] headsetid it's on the sticker in the battery-case
135
+ # @param [String] device tty-device
136
+ # @param [Integer] rate baud-rate
137
+ # @param [Logger] log (logger-instance)
138
+ def initialize(headsetid=nil,device='/dev/ttyUSB0', connectserial=true,rate=115200, log=Logger.new(STDOUT))
139
+ @headsetid=headsetid
140
+ @device=device
141
+ @rate=rate
142
+ @log=log
143
+ @log.level = Logger::FATAL
144
+ @headsetstatus = 0
145
+ @runner = true
146
+
147
+ if connectserial
148
+ serial_open
149
+ connect(@headsetid)
150
+ end
151
+ end
152
+
153
+ # connects the Mindwave-headset(not needed with Mindwave-Mobile)
154
+ # (Mindwave only)
155
+ #
156
+ # @param [Integer] headsetid it's on the sticker in the battery-case
157
+ # TODO: implement connection with headsetid
158
+ def connect(headsetid=nil)
159
+ if not headsetid.nil?
160
+ @headsetid = headsetid
161
+ end
162
+
163
+ if @headsetid.nil?
164
+ autoconnect()
165
+ return
166
+ end
167
+
168
+ cmd = BinData::Uint8be.new(Mindwave::Headset::CONNECT)
169
+ cmd.write(@conn)
170
+ end
171
+
172
+ # This method creates an infinite loop
173
+ # and reads out all data from the headset using
174
+ # the open serial-line.
175
+ def run
176
+
177
+ tmpbyte = 0;
178
+ @runner = true
179
+
180
+ while @runner
181
+ log.debug("<<< START RECORD >>>")
182
+ tmpbyte = logreadbyte
183
+
184
+ # 0xaa indicates the first start of a packet
185
+ if tmpbyte != Mindwave::Headset::SYNC
186
+ log.info(sprintf("LOST: %x\n",tmpbyte))
187
+ next
188
+ else
189
+ tmpbyte = logreadbyte()
190
+ # a second 0xaa verifies the start of a packet
191
+ if tmpbyte != Mindwave::Headset::SYNC
192
+ log.info(sprintf("LOST: %x\n",tmpbyte))
193
+ next
194
+ end
195
+
196
+ end
197
+
198
+ while true
199
+ # read out the length of the packet
200
+ plength = logreadbyte()
201
+ if(plength != 170)
202
+ break
203
+ end
204
+ end
205
+
206
+ if(plength > 170)
207
+ next
208
+ end
209
+
210
+ log.info(sprintf("Header-Length: %d",plength))
211
+ payload = Array.new(plength)
212
+ checksum = 0
213
+ # read out payload
214
+ (0..plength-1).each do |n|
215
+ payload[n] = logreadbyte()
216
+ # ..and add it to the checksum
217
+ checksum += payload[n]
218
+ end
219
+
220
+ # weird checksum calculations
221
+ checksum &= 0xff
222
+ checksum = ~checksum & 0xff
223
+
224
+ # read checksum-packet
225
+ c = logreadbyte()
226
+
227
+ # compare checksum-packet with the calculated checksum
228
+ if( c != checksum)
229
+ log.info(sprintf("Checksum Error: %x - %x\n",c,checksum))
230
+ else
231
+ # so finally parse the payload of our packet
232
+ parse_payload(payload)
233
+ end
234
+
235
+ end
236
+
237
+ end
238
+
239
+ # this method parses the payload of a data-row, parses the values and invokes the callback methods
240
+ # @param [Array] payload Array with the payload
241
+ def parse_payload(payload)
242
+ if not payload.instance_of?Array or payload.nil? or payload.length < 2
243
+ raise "Invalid Argument"
244
+ end
245
+
246
+ log.info("####### PARSE PAYLOAD #########")
247
+
248
+ extcodelevel = 0
249
+
250
+ # parse the first code and it's payload
251
+ code = payload[0]
252
+ pl = payload[1,payload.length-1]
253
+
254
+ if code == Mindwave::Headset::EXCODE
255
+ extcodelevel += 1
256
+
257
+ # iterate through the payload-array
258
+ (1..payload.length).each do |n|
259
+ # if there is an excode, increment the level
260
+ if payload[n] == Mindwave::Headset::EXCODE
261
+ extcodelevel += 1
262
+ else
263
+ # ..otherwise parse the next code and it's payload
264
+ code = payload[n]
265
+ pl = payload[n+1,payload.length-(n+1)]
266
+ break
267
+ end
268
+ end
269
+ end
270
+
271
+ # some debugging output
272
+ log.info(sprintf("extcodelevel: %x",extcodelevel))
273
+ log.info(sprintf("Code: %x",code))
274
+ log.debug(sprintf("Length: %d",pl.length))
275
+ pl.each do |n|
276
+ log.debug(sprintf("payload: Hex: %x Dec: %d",n,n))
277
+ end
278
+
279
+
280
+ # SINGLE-BYTE-CODES
281
+ if code < Mindwave::Headset::RAW_WAVE or code >= Mindwave::Headset::HEADSET_CONNECTED
282
+
283
+ sbpayload = pl[0]
284
+ codestr = ""
285
+
286
+ case code
287
+ when Mindwave::Headset::HEADSET_CONNECTED
288
+ codestr = "Headset connected"
289
+ @headsetstatus = code
290
+ when Mindwave::Headset::HEADSET_NOTFOUND
291
+ codestr = "Headset not found"
292
+ @headsetstatus = code
293
+ when Mindwave::Headset::HEADSET_DISCONNECTED
294
+ codestr = "Headset disconnected"
295
+ @headsetstatus = code
296
+ when Mindwave::Headset::REQUEST_DENIED
297
+ codestr = "Request denied"
298
+ @headsetstatus = code
299
+ when Mindwave::Headset::DONGLE_STANDBY
300
+ codestr = "Dongle standby"
301
+ @headsetstatus = code
302
+ when Mindwave::Headset::POOR_SIGNAL
303
+ codestr = "Poor Signal"
304
+ @poor = sbpayload
305
+ poorCall(@poor)
306
+ when Mindwave::Headset::HEART_RATE
307
+ codestr = "Heart Rate"
308
+ @heart = sbpayload
309
+ heartCall(@heart)
310
+ when Mindwave::Headset::ATTENTION
311
+ codestr = "Attention"
312
+ @attention = sbpayload
313
+ attentionCall(@attention)
314
+ when Mindwave::Headset::MEDITATION
315
+ codestr = "Meditation"
316
+ @meditation = sbpayload
317
+ meditationCall(@meditation)
318
+ ## THIS METHODS ARE NOT AVAILABLE FOR MINDWAVE(MOBILE)
319
+ when Mindwave::Headset::BIT8_RAW
320
+ codestr = "8Bit Raw"
321
+ when Mindwave::Headset::RAW_MARKER
322
+ codestr = "Raw Marker"
323
+ # EOF NOT AVAILABLE
324
+ else
325
+ codestr = "Unknown"
326
+ end
327
+
328
+ log.debug(sprintf("SINGLEBYTE-PAYLOAD: Code: %s Hex: %x - Dec: %d",codestr,sbpayload,sbpayload))
329
+
330
+ # Re-Parse the rest of the payload
331
+ if pl.length > 2
332
+ payload = pl[1,pl.length-1]
333
+ # recursive call of parse_payload for the next data-rows
334
+ parse_payload(payload)
335
+ end
336
+
337
+ # MULTI-BYTE-CODES
338
+ else
339
+ codestr = ""
340
+ plength = pl[0]
341
+ mpl = pl[1,plength]
342
+
343
+ case code
344
+
345
+ when Mindwave::Headset::RAW_WAVE
346
+ codestr = "RAW_WAVE Code detected"
347
+ rawCall(convertRaw(mpl[0],mpl[1]))
348
+ when Mindwave::Headset::EEG_POWER
349
+ codestr = "EEG Power"
350
+ when Mindwave::Headset::ASIC_EEG_POWER
351
+ codestr = "ASIC EEG POWER"
352
+ @asic = mpl
353
+ asicCall(@asic)
354
+ when Mindwave::Headset::RRINTERVAL
355
+ codestr = "RRINTERVAL"
356
+ else
357
+ codestr = "Unknown"
358
+ end
359
+
360
+ # Fetch the Multi-Payload
361
+ log.info(sprintf("Multibyte-Code: %s",codestr))
362
+ log.info(sprintf("Multibyte-Payload-Length: %d",pl[0]))
363
+
364
+ mpl.each() do |n|
365
+ log.debug(sprintf("MULTIBYTE-PAYLOAD: Hex: %x - Dec: %d",n,n))
366
+ end
367
+
368
+ # Re-Parse the rest of the payload
369
+ if pl.length-1 > plength
370
+ payload = pl[mpl.length+1,pl.length-mpl.length]
371
+ # recursive call of parse_payload for the next data-rows
372
+ parse_payload(payload)
373
+ end
374
+ end
375
+
376
+
377
+ end
378
+
379
+ # this method sends a byte to the serial connection
380
+ # (Mindwave only)
381
+ #
382
+ # @param [Integer] hexbyte byte to send
383
+ def sendbyte(hexbyte)
384
+ cmd = BinData::Uint8be.new(hexbyte)
385
+ cmd.write(@conn)
386
+ end
387
+
388
+ # This method connects to the headset automatically without knowing the device-id
389
+ # (Mindwave only)
390
+ def autoconnect
391
+ cmd = BinData::Uint8be.new(Mindwave::Headset::AUTOCONNECT)
392
+ cmd.write(@conn)
393
+ end
394
+
395
+ # this method disconnects a connection between headset and dongle
396
+ # (Mindwave only)
397
+ def disconnect
398
+ cmd = BinData::Uint8be.new(Mindwave::Headset::DISCONNECT)
399
+ cmd.write(@conn)
400
+ end
401
+
402
+ # this method opens a serial connection to the device
403
+ def serial_open
404
+ @conn = SerialPort.new(@device,@rate)
405
+ end
406
+
407
+ # this method closes a serial connection to the device
408
+ def serial_close
409
+ @conn.close
410
+ end
411
+
412
+ # this method disconnects the headset and closes the serial line
413
+ def close
414
+ disconnect
415
+ serial_close
416
+ end
417
+
418
+ # this method stops the run-method
419
+ def stop
420
+ @runner = false
421
+ end
422
+
423
+ # --------------------------
424
+ # :section: Callback Methods
425
+ # --------------------------
426
+
427
+ # this method is called when the poor-value is parsed
428
+ # override this method to implement your own clode
429
+ #
430
+ # @param [Integer] poor poor-value
431
+ def poorCall(poor)
432
+ if poor == 200
433
+ log.info("No skin-contact detected")
434
+ end
435
+ end
436
+
437
+ # this method is called when the attention-value is parsed
438
+ # override this method to implement your own code
439
+ #
440
+ # @param [Integer] attention attention-value
441
+ def attentionCall(attention)
442
+ str = eSenseStr(attention)
443
+ log.info("ATTENTION #{attention} #{str}")
444
+ end
445
+
446
+ # this method is called when the meditation-value is parsed
447
+ # override this method to implement your own code
448
+ #
449
+ # @param [Integer] meditation meditation-value
450
+ def meditationCall(meditation)
451
+ str = eSenseStr(meditation)
452
+ log.info("MEDITATION #{meditation} #{str}")
453
+ end
454
+
455
+ # this method is called when the heart-rate-value is parsed
456
+ # override this method to implement your own code
457
+ #
458
+ # @param [Integer] heart heart-value
459
+ def heartCall(heart)
460
+ log.info("HEART RATE #{heart}")
461
+ end
462
+
463
+ # this method is called when the raw-wave-value is parsed
464
+ # override this method to implement your own code
465
+ #
466
+ # @param [Integer] rawvalue raw-wave-value
467
+ def rawCall(rawvalue)
468
+ log.debug("Converted Raw-Value: #{rawvalue}")
469
+ end
470
+
471
+ ##
472
+ # this method is called when the asic-value is parsed
473
+ # override this method to implement your own code
474
+ #
475
+ # @param [Integer] asic asic-value
476
+ #
477
+ def asicCall(asic)
478
+ log.debug("ASIC Value: #{asic}")
479
+ end
480
+
481
+ private
482
+
483
+ # reads out a byte from the serial-line and
484
+ # logs the byte using "debug"
485
+ def logreadbyte
486
+ begin
487
+ ret = @conn.readbyte
488
+ rescue EOFError
489
+ log.fatal("EOFError")
490
+ # But Ignore it with FF
491
+ ret = 0x00
492
+ end
493
+ log.debug(sprintf("HEX: %x DEC: %d",ret,ret))
494
+ return ret
495
+ end
496
+
497
+ # this method converts the numeric eSense-value of attention or meditation
498
+ # to a string
499
+ #
500
+ # @param [Integer] value eSense-value
501
+ # @returns [String] string-value
502
+ # = eSense Values(Attention and Meditation)
503
+ # * 1-20 = strongly lowered
504
+ # * 20-40 = reduced
505
+ # * 40-60 = neutral
506
+ # * 60-80 = slightly elevated
507
+ # * 80-100 = elevated
508
+ def eSenseStr(value)
509
+ result = case value
510
+ when 0..20 then "Strongly lowered"
511
+ when 21..40 then "Reduced"
512
+ when 41..60 then "Neutral"
513
+ when 61..80 then "Slightly elevated"
514
+ when 81..100 then "Elevated"
515
+ else
516
+ "Unknown"
517
+ end
518
+
519
+ return result
520
+ end
521
+
522
+ # converts a raw-wave-data-packet of 2 bytes to a single value
523
+ #
524
+ # @param [Integer] rawval1 first byte-packet of the raw-wave-code
525
+ # @param [Integer] rawval2 second byte-packet of the raw-wave-code
526
+ #
527
+ # @return [Integer] single value generated from the 2 bytes
528
+ def convertRaw(rawval1,rawval2)
529
+ raw = rawval1*256 + rawval2
530
+ if raw >= 32768
531
+ raw = raw - 65536
532
+ end
533
+
534
+ return raw
535
+ end
536
+
537
+ end
538
+ end