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.
- checksums.yaml +7 -0
- data/lib/mindset.rb +325 -0
- metadata +49 -0
checksums.yaml
ADDED
@@ -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
|
data/lib/mindset.rb
ADDED
@@ -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: []
|