hybridgroup-mindset 0.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.
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: []