mindwave 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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