hybridgroup-mindset 0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/mindset.rb +325 -0
  3. metadata +49 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fce0bfce1670cd37613af31635c7e866cf865910
4
+ data.tar.gz: dea9314428aca28b7323fd4fc3fa8fb7284ae56a
5
+ SHA512:
6
+ metadata.gz: ebe19da7017bfd37c12f3830227d7953d2c3de2a9bfa2a79a4bab39f45b0360663c5a958f087de307ac0339a11905d5a9787939423876d36b89a18c5491524b1
7
+ data.tar.gz: d1e2b59b142aca966019be8fba67a24535399122e1f6f422ee9f1089c8f41fcb70f490246f883683064739aa724049e5de898030bf0ce06b6db598f0a80ff79b
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env ruby
2
+ # Ruby module for reading data from a Neurosky Mindset.
3
+ # (c) Copyright 2013 mkfs@github http://github.com/mkfs/mindset
4
+ # License: BSD http://www.freebsd.org/copyright/freebsd-license.html
5
+
6
+ require 'rubygems' # gem install serialport
7
+ require 'json/ext'
8
+
9
+ # ----------------------------------------------------------------------
10
+ module Mindset
11
+
12
+ =begin rdoc
13
+ Collection of captured Packet objects. Packets are collected by type. The
14
+ start and end timestamps are saved.
15
+ =end
16
+ class PacketStore < Hash
17
+ def initialize
18
+ super
19
+
20
+ self[:start_ts] = Time.now
21
+ self[:end_ts] = nil
22
+ self[:delta] = []
23
+ self[:theta] = []
24
+ self[:lo_alpha] = []
25
+ self[:hi_alpha] = []
26
+ self[:lo_beta] = []
27
+ self[:hi_beta] = []
28
+ self[:lo_gamma] = []
29
+ self[:mid_gamma] = []
30
+ self[:signal_quality] = []
31
+ self[:attention] = []
32
+ self[:meditation] = []
33
+ self[:blink] = []
34
+ self[:wave] = []
35
+ end
36
+
37
+ def to_json
38
+ super
39
+ end
40
+
41
+ end
42
+
43
+ # ----------------------------------------------------------------------
44
+ =begin rdoc
45
+ A Mindset data packet.
46
+ This is usually either a Raw data packet, an eSense packet, or an ASIC EEG
47
+ packet.
48
+ =end
49
+ class Packet < Hash
50
+ EXCODE = 0x55 # Extended code
51
+ CODE_SIGNAL_QUALITY = 0x02 # POOR_SIGNAL quality 0-255
52
+ CODE_ATTENTION = 0x04 # ATTENTION eSense 0-100
53
+ CODE_MEDITATION = 0x05 # MEDITATION eSense 0-100
54
+ CODE_BLINK = 0x16 # BLINK strength 0-255
55
+ CODE_WAVE = 0x80 # RAW wave value: 2-byte big-endian 2s-complement
56
+ CODE_ASIC_EEG = 0x83 # ASIC EEG POWER 8 3-byte big-endian integers
57
+
58
+ # returns array of packets
59
+ def self.parse(bytes, verbose=false)
60
+ packets = []
61
+ while not bytes.empty?
62
+ excode = 0
63
+ while bytes[0] == EXCODE
64
+ excode += 1
65
+ bytes.shift
66
+ end
67
+
68
+ code = bytes.shift
69
+ vlen = (code >= 0x80) ? bytes.shift : 1
70
+ value = bytes.slice! 0, vlen
71
+ pkt = Packet.new
72
+ pkt.decode(excode, code, value, verbose)
73
+ packets << pkt
74
+ end
75
+
76
+ packets
77
+ end
78
+
79
+ def self.decode(excode, code, value, verbose=false)
80
+ Packet.new.decode(excode, code, value, verbose)
81
+ end
82
+
83
+ def self.factory(name, value)
84
+ pkt = self.new
85
+ pkt[name.to_sym] = value
86
+ pkt
87
+ end
88
+
89
+ def decode(excode, code, value, verbose=nil)
90
+ # note: currently, excode is ignored
91
+ case code
92
+ when CODE_SIGNAL_QUALITY
93
+ self[:signal_quality] = value.first
94
+ when CODE_ATTENTION
95
+ self[:attention] = value.first
96
+ when CODE_MEDITATION
97
+ self[:meditation] = value.first
98
+ when CODE_BLINK
99
+ self[:blink] = value.first
100
+ when CODE_WAVE
101
+ self[:wave] = value[0,2].join('').unpack("s>").first
102
+ when CODE_ASIC_EEG
103
+ unpack_asic_eeg(value[0,24])
104
+ else
105
+ $stderr.puts "Unrecognized code: %02X" % code if verbose
106
+ end
107
+ self
108
+ end
109
+
110
+ def is_asic_wave?
111
+ ([:lo_beta, :hi_beta, :delta, :lo_gamma, :theta, :mid_gamma, :lo_alpha,
112
+ :hi_alpha] & self.keys).count > 0
113
+ end
114
+
115
+ def is_esense?
116
+ (self.keys.include? :attention) || (self.keys.include? :meditation)
117
+ end
118
+
119
+ def to_json
120
+ super
121
+ end
122
+
123
+ private
124
+
125
+ # Treat 3-element array as a 3-byte unsigned integer in little-endian order
126
+ def unpack_3byte_bigendian(arr)
127
+ arr.push(0).pack('cccc').unpack('L<').first
128
+ end
129
+
130
+ def unpack_asic_eeg(arr)
131
+ self[:delta] = unpack_3byte_bigendian(arr[0,3])
132
+ self[:theta] = unpack_3byte_bigendian(arr[3,3])
133
+ self[:lo_alpha] = unpack_3byte_bigendian(arr[6,3])
134
+ self[:hi_alpha] = unpack_3byte_bigendian(arr[9,3])
135
+ self[:lo_beta] = unpack_3byte_bigendian(arr[12,3])
136
+ self[:hi_beta] = unpack_3byte_bigendian(arr[15,3])
137
+ self[:lo_gamma] = unpack_3byte_bigendian(arr[18,3])
138
+ self[:mid_gamma] = unpack_3byte_bigendian(arr[21,3])
139
+ end
140
+ end
141
+
142
+ # ----------------------------------------------------------------------
143
+ =begin rdoc
144
+ A connection to a Mindset device. This wraps the SerialPort connection to the
145
+ device. Device must already be paired and have a serial bluetooth connection
146
+ established.
147
+ =end
148
+ class Connection
149
+ SERIAL_PORT = "/dev/rfcomm0"
150
+ BAUD_RATE = 57600
151
+ BT_SYNC = 0xAA
152
+
153
+ class TimeoutError < RuntimeError; end
154
+
155
+ def initialize(device=nil)
156
+ if device.is_a?(String)
157
+ initialize_serialport device
158
+ elsif device.nil?
159
+ initialize_serialport SERIAL_PORT
160
+ else
161
+ @sp = device
162
+ end
163
+ # Note: Mutex causes crashes when used with qtbindings
164
+ @locked = false
165
+ end
166
+
167
+
168
+ def initialize_serialport dev
169
+ require 'serialport'
170
+ @sp = SerialPort.new dev, BAUD_RATE, 8, 1, SerialPort::NONE
171
+ if is_windows?
172
+ @sp.read_timeout=1000
173
+ @sp.write_timeout=0
174
+ @sp.initial_byte_offset=5
175
+ end
176
+ rescue LoadError
177
+ puts "Please 'gem install hybridgroup-serialport' for serial port support."
178
+ end
179
+
180
+ =begin rdoc
181
+ Return an Array of Packet objects.
182
+ Note: this will perform a blocking read on the serial device.
183
+ =end
184
+ def read_packet(verbose=false)
185
+ return [] if @locked
186
+ @locked = true
187
+
188
+ pkts = []
189
+ if wait_for_byte(BT_SYNC) and wait_for_byte(BT_SYNC)
190
+ plen = @sp.getbyte
191
+ if plen and plen < BT_SYNC
192
+ pkts = read_payload(plen, verbose)
193
+ else
194
+ $stderr.puts "Invalid packet size: #{plen} bytes" if verbose
195
+ end
196
+ end
197
+ @locked = false
198
+ pkts
199
+ end
200
+
201
+ def disconnect
202
+ @sp.close
203
+ end
204
+
205
+ private
206
+
207
+ def read_payload(plen, verbose=false)
208
+ str = @sp.read(plen)
209
+ buf = str ? str.bytes.to_a : []
210
+
211
+ checksum = @sp.getbyte
212
+
213
+ buf_cs = buf.inject(0) { |sum, b| sum + b } & 0xFF
214
+ buf_cs = ~buf_cs & 0xFF
215
+ if (! checksum) or buf_cs != checksum
216
+ $stderr.puts "Packet #{buf_cs} != checksum #{checkum}" if verbose
217
+ return []
218
+ end
219
+
220
+ pkts = Packet.parse buf, verbose
221
+ end
222
+
223
+ def wait_for_byte(val, max_counter=500)
224
+ max_counter.times do
225
+ c = @sp.getbyte
226
+ return true if (c == val)
227
+ end
228
+ false
229
+ end
230
+
231
+ def is_windows?
232
+ os = RUBY_PLATFORM.split("-")[1]
233
+ if (os == 'mswin' or os == 'bccwin' or os == 'mingw' or os == 'mingw32')
234
+ true
235
+ else
236
+ false
237
+ end
238
+ end
239
+ end
240
+
241
+ =begin rdoc
242
+ A fake Mindset connection which just replays data previously captured (and
243
+ serialized to JSON).
244
+ This is used to provide a uniform interface for displaying either realtime or
245
+ captured EEG data.
246
+ Note: This expects a PacketStore object to be stored in @data before read_packet
247
+ is called.
248
+ =end
249
+ class LoopbackConnection
250
+ attr_accessor :data
251
+
252
+ def initialize(data=nil)
253
+ @data = data
254
+ @counter = 0
255
+ @wave_idx = 0
256
+ @esense_idx = 0
257
+ end
258
+
259
+ =begin rdoc
260
+ Simulate a read of the Mindset device by returning an Array of Packet objects.
261
+ This assumes it will be called 8 times a second.
262
+
263
+ According to the MDT, Mindset packets are sent at the following intervals:
264
+ 1 packet per second: eSense, ASIC EEG, POOR_SIGNAL
265
+ 512 packets per second: RAW
266
+
267
+ Each read will therefore return 64 RAW packets. Every eighth read will also
268
+ return 1 eSense, ASIC_EEG, and POOR_SIGNAL packet.
269
+ =end
270
+ def read_packet(verbose=false)
271
+ packets = @data[:wave][@wave_idx, 64].map { |val|
272
+ Packet.factory(:wave, val) }
273
+ @wave_idx += 64
274
+ @wave_idx = 0 if @wave_idx >= @data[:wave].count
275
+
276
+ if @counter == 7
277
+ packets << Packet.factory(:delta, @data[:delta][@esense_idx])
278
+ packets << Packet.factory(:theta, @data[:theta][@esense_idx])
279
+ packets << Packet.factory(:lo_alpha, @data[:lo_alpha][@esense_idx])
280
+ packets << Packet.factory(:hi_alpha, @data[:hi_alpha][@esense_idx])
281
+ packets << Packet.factory(:lo_beta, @data[:lo_beta][@esense_idx])
282
+ packets << Packet.factory(:hi_beta, @data[:hi_beta][@esense_idx])
283
+ packets << Packet.factory(:lo_gamma, @data[:lo_gamma][@esense_idx])
284
+ packets << Packet.factory(:mid_gamma, @data[:mid_gamma][@esense_idx])
285
+ packets << Packet.factory(:signal_quality,
286
+ @data[:signal_quality][@esense_idx])
287
+ packets << Packet.factory(:attention, @data[:attention][@esense_idx])
288
+ packets << Packet.factory(:meditation, @data[:meditation][@esense_idx])
289
+ packets << Packet.factory(:blink, @data[:blink][@esense_idx])
290
+ @esense_idx += 1
291
+ @esense_idx = 0 if @esense_idx >= @data[:delta].count
292
+ end
293
+
294
+ @counter = (@counter + 1) % 8
295
+ packets
296
+ end
297
+
298
+ def disconnect
299
+ @data = {}
300
+ end
301
+ end
302
+
303
+ # ----------------------------------------------------------------------
304
+ =begin rdoc
305
+ Return a Mindset::Connection object for device.
306
+ If a block is provided, this yields the Connection object, then disconnects it
307
+ when the block returns.
308
+ =end
309
+ def self.connect(device, verbose=false, &block)
310
+ $stderr.puts "CONNECT #{device}, #{MINDSET_BAUD}" if verbose
311
+ begin
312
+ conn = Connection.new device
313
+ if block_given?
314
+ yield conn
315
+ conn.disconnect
316
+ else
317
+ return conn
318
+ end
319
+ rescue TypeError => e
320
+ $stderr.puts "Could not connect to #{device}: #{e.message}"
321
+ end
322
+ nil
323
+ end
324
+
325
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hybridgroup-mindset
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.3'
5
+ platform: ruby
6
+ authors:
7
+ - mkfs
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: "This module reads all data transmitted by a Neurosky\nMindset, which
14
+ must already be paired in Bluetooth and have a Bluetooth serial\n(SPP) socket open.
15
+ The gem provides the module and a command line utility for\nreading packets and
16
+ either printing them to STDOUT in realtime, or serializing \nthem to STDOUT in JSON
17
+ on exit."
18
+ email: see.url@github.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - lib/mindset.rb
24
+ homepage: https://github.com/mkfs/mindset
25
+ licenses:
26
+ - BSD
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - '>='
35
+ - !ruby/object:Gem::Version
36
+ version: 1.9.2
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements:
43
+ - '''json'' gem >= 1.5.0'
44
+ rubyforge_project:
45
+ rubygems_version: 2.2.1
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: Read data from a Neurosky Mindset
49
+ test_files: []