hybridgroup-sphero 1.1.6 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -77,6 +77,23 @@ sleep 1
77
77
  s.stop
78
78
  ```
79
79
 
80
+ ## Pairing sphero with ubuntu
81
+ Add your user to the `dialout` group
82
+ ```
83
+ $ sudo usermod -a -G dialout <user>
84
+ ```
85
+ Then logout or restart your computer. Once your user is logged back in, pair the sphero with the ubuntu bluetooth manager.
86
+
87
+ Once paired, you may now bind your sphero to a rfcomm port
88
+ ```
89
+ $ sudo hcitool scan
90
+ Scanning ...
91
+ <address> Sphero
92
+ $ sudo rfcomm bind /dev/rfcomm0 <address> 1
93
+ ```
94
+
95
+ You may now access the sphero from `/dev/rfcomm0`
96
+
80
97
  ## REQUIREMENTS:
81
98
 
82
99
  * A Sphero ball connected to your computer
data/Rakefile CHANGED
@@ -14,7 +14,6 @@ Hoe.spec 'hybridgroup-sphero' do
14
14
  self.readme_file = 'README.markdown'
15
15
  self.history_file = 'CHANGELOG.rdoc'
16
16
  self.extra_rdoc_files = FileList['*.{rdoc,markdown}']
17
- self.extra_deps << ['hybridgroup-serialport']
18
17
 
19
18
  self.spec_extras = {
20
19
  :required_ruby_version => '>= 1.9.2'
@@ -113,5 +113,73 @@ class Sphero
113
113
  @data.pack 'nC'
114
114
  end
115
115
  end
116
+
117
+ class SetPowerNotification < Sphero
118
+ def initialize seq, enable
119
+ super(seq, [enable])
120
+ @cid = 0x21
121
+ end
122
+ end
123
+
124
+ GYRO_AXIS_H_FILTERED = 0x0000_0001
125
+ GYRO_AXIS_M_FILTERED = 0x0000_0002
126
+ GYRO_AXIS_L_FILTERED = 0x0000_0004
127
+ LEFT_MOTOR_BACK_EMF_FILTERED = 0x0000_0020
128
+ RIGHT_MOTOR_BACK_EMF_FILTERED = 0x0000_0040
129
+ MAGNETOMETER_AXIS_Z_FILTERED = 0x0000_0080
130
+ MAGNETOMETER_AXIS_Y_FILTERED = 0x0000_0100
131
+ MAGNETOMETER_AXIS_X_FILTERED = 0x0000_0200
132
+ GYRO_AXIS_Z_FILTERED = 0x0000_0400
133
+ GYRO_AXIS_Y_FILTERED = 0x0000_0800
134
+ GYRO_AXIS_X_FILTERED = 0x0000_1000
135
+ ACCELEROMETER_AXIS_Z_FILTERED = 0x0000_2000
136
+ ACCELEROMETER_AXIS_Y_FILTERED = 0x0000_4000
137
+ ACCELEROMETER_AXIS_X_FILTERED = 0x0000_8000
138
+ IMU_YAW_ANGLE_FILTERED = 0x0001_0000
139
+ IMU_ROLL_ANGLE_FILTERED = 0x0002_0000
140
+ IMU_PITCH_ANGLE_FILTERED = 0x0004_0000
141
+ LEFT_MOTOR_BACK_EMF_RAW = 0x0020_0000
142
+ RIGHT_MOTOR_BACK_EMF_RAW = 0x0040_0000
143
+ MAGNETOMETER_AXIS_Z_RAW = 0x0080_0000
144
+ MAGNETOMETER_AXIS_Y_RAW = 0x0100_0000
145
+ MAGNETOMETER_AXIS_X_RAW = 0x0200_0000
146
+ GYRO_AXIS_Z_RAW = 0x0400_0000
147
+ GYRO_AXIS_Y_RAW = 0x0800_0000
148
+ GYRO_AXIS_X_RAW = 0x1000_0000
149
+ ACCELEROMETER_AXIS_Z_RAW = 0x2000_0000
150
+ ACCELEROMETER_AXIS_Y_RAW = 0x4000_0000
151
+ ACCELEROMETER_AXIS_X_RAW = 0x8000_0000
152
+
153
+ QUATERNION_Q0 = 0x0000_0001
154
+ QUATERNION_Q1 = 0x0000_0002
155
+ QUATERNION_Q2 = 0x0000_0004
156
+ QUATERNION_Q3 = 0x0000_0008
157
+ ODOMETER_X = 0x0000_0010
158
+ ODOMETER_Y = 0x0000_0020
159
+ ACCELONE = 0x0000_0040
160
+ VELOCITY_X = 0x0000_0080
161
+ VELOCITY_Y = 0x0000_0100
162
+
163
+ class SetDataStreaming < Sphero
164
+ def initialize seq, n, m, mask, pcnt, mask2
165
+ super(seq, [n, m, mask, pcnt, mask2])
166
+ @cid = 0x12
167
+ @mask = mask
168
+ @mask2 = mask2
169
+ end
170
+
171
+ private
172
+
173
+ def packet_body
174
+ @data.pack 'nnNCN'
175
+ end
176
+ end
177
+
178
+ class ConfigureCollisionDetection < Sphero
179
+ def initialize seq, meth, x_t, y_t, x_spd, y_spd, dead
180
+ super(seq, [meth, x_t, y_t, x_spd, y_spd, dead])
181
+ @cid = 0x12
182
+ end
183
+ end
116
184
  end
117
185
  end
@@ -7,22 +7,36 @@ class Sphero
7
7
  DLEN = 4
8
8
 
9
9
  CODE_OK = 0
10
+ SIMPLE_RESPONSE = 0xFF
11
+ ASYNC_RESPONSE = 0xFE
12
+
13
+ def self.simple?(header)
14
+ header[SOP2] == SIMPLE_RESPONSE
15
+ end
16
+
17
+ def self.async?(header)
18
+ header[SOP2] == ASYNC_RESPONSE
19
+ end
10
20
 
11
21
  def initialize header, body
12
22
  @header = header
13
23
  @body = body
14
24
  end
15
25
 
26
+ def valid?
27
+ @header && @body
28
+ end
29
+
16
30
  def empty?
17
- @header[DLEN] == 1
31
+ valid? && @header[DLEN] == 1
18
32
  end
19
33
 
20
34
  def success?
21
- @header[MRSP] == CODE_OK
35
+ valid? && @header[MRSP] == CODE_OK
22
36
  end
23
37
 
24
38
  def seq
25
- @header[SEQ]
39
+ valid? && @header[SEQ]
26
40
  end
27
41
 
28
42
  def body
@@ -84,5 +98,106 @@ class Sphero
84
98
  def g; body[1]; end
85
99
  def b; body[2]; end
86
100
  end
101
+
102
+ class AsyncResponse < Response
103
+ ID_CODE = 2
104
+ DLEN_MSB = 3
105
+ DLEN_LSB = 4
106
+
107
+ POWER_NOTIFICATION = 0x01
108
+ LEVEL_1_DIAGNOSTIC = 0x02
109
+ SENSOR_DATA = 0x03
110
+ CONFIG_BLOCK = 0x04
111
+ PRESLEEP_WARNING = 0x05
112
+ MACRO_MARKERS = 0x06
113
+ COLLISION_DETECTED = 0x07
114
+
115
+ VALID_REPONSES = {POWER_NOTIFICATION => 'Sphero::Response::PowerNotification',
116
+ #LEVEL_1_DIAGNOSTIC => 'AsyncResponse',
117
+ SENSOR_DATA => 'Sphero::Response::SensorData',
118
+ #CONFIG_BLOCK => 'AsyncResponse',
119
+ #PRESLEEP_WARNING => 'AsyncResponse',
120
+ #MACRO_MARKERS => 'AsyncResponse',
121
+ COLLISION_DETECTED => 'Sphero::Response::CollisionDetected'}
122
+
123
+ def self.valid?(header)
124
+ VALID_REPONSES.keys.include?(header[ID_CODE])
125
+ end
126
+
127
+ def self.response header, body
128
+ raise "no good" unless self.valid?(header)
129
+ constantize(VALID_REPONSES[header[ID_CODE]]).new(header, body)
130
+ end
131
+
132
+ def self.constantize(camel_cased_word)
133
+ names = camel_cased_word.split('::')
134
+ names.shift if names.empty? || names.first.empty?
135
+
136
+ constant = Object
137
+ names.each do |name|
138
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
139
+ end
140
+ constant
141
+ end
142
+
143
+ def empty?
144
+ @header[DLEN_LSB] == 1
145
+ end
146
+
147
+ def success?
148
+ AsyncResponse.valid?(@header)
149
+ end
150
+
151
+ def seq
152
+ 1
153
+ end
154
+ end
155
+
156
+ class PowerNotification < GetPowerState
157
+ end
158
+
159
+ class SensorData < AsyncResponse
160
+ def body
161
+ @body.unpack 's*'
162
+ end
163
+ end
164
+
165
+ class CollisionDetected < AsyncResponse
166
+ def body
167
+ @body.unpack 'nnnCnnCN'
168
+ end
169
+
170
+ def x
171
+ body[0]
172
+ end
173
+
174
+ def y
175
+ body[1]
176
+ end
177
+
178
+ def z
179
+ body[2]
180
+ end
181
+
182
+ def axis
183
+ body[3]
184
+ end
185
+
186
+ def x_magnitude
187
+ body[4]
188
+ end
189
+
190
+ def y_magnitude
191
+ body[5]
192
+ end
193
+
194
+ def speed
195
+ body[6]
196
+ end
197
+
198
+ def timestamp
199
+ body[7]
200
+ end
201
+ end
87
202
  end
88
203
  end
data/lib/sphero.rb CHANGED
@@ -1,18 +1,22 @@
1
- require 'serialport'
2
1
  require 'sphero/request'
3
2
  require 'sphero/response'
4
3
  require 'thread'
5
4
 
6
5
  class Sphero
7
- VERSION = '1.1.6'
6
+ VERSION = '1.2.0'
8
7
 
9
8
  FORWARD = 0
10
9
  RIGHT = 90
11
10
  BACKWARD = 180
12
11
  LEFT = 270
13
12
 
13
+ DEFAULT_RETRIES = 3
14
+
15
+ attr_accessor :connection_types, :async_messages
16
+
14
17
  class << self
15
18
  def start(dev, &block)
19
+ retries_left = DEFAULT_RETRIES
16
20
  sphero = self.new dev
17
21
  if (block_given?)
18
22
  begin
@@ -24,15 +28,23 @@ class Sphero
24
28
  end
25
29
  return sphero
26
30
  rescue Errno::EBUSY
27
- retry
31
+ puts retries_left
32
+ retries_left = retries_left - 1
33
+ retry unless retries_left > 0
28
34
  end
29
35
  end
30
36
 
31
37
  def initialize dev
32
- initialize_serialport dev
38
+ if dev.is_a?(String)
39
+ initialize_serialport dev
40
+ else
41
+ @sp = dev
42
+ end
43
+
33
44
  @dev = 0x00
34
45
  @seq = 0x00
35
46
  @lock = Mutex.new
47
+ @async_messages = []
36
48
  end
37
49
 
38
50
  def close
@@ -110,6 +122,42 @@ class Sphero
110
122
  Kernel::sleep duration
111
123
  end
112
124
 
125
+ ## async messages
126
+
127
+ # configure power notification messages
128
+ def set_power_notification enable=true
129
+ write Request::SetPowerNotification.new(@seq, enable ? 0x01 : 0x00)
130
+ end
131
+
132
+ # configure data streaming notification messages
133
+ def set_data_streaming n, m, mask, pcnt, mask2
134
+ write Request::SetDataStreaming.new(@seq, n, m, mask, pcnt, mask2)
135
+ end
136
+
137
+ # configure collision detection messages
138
+ def configure_collision_detection meth, x_t, y_t, x_spd, y_spd, dead
139
+ write Request::ConfigureCollisionDetection.new(@seq, meth, x_t, y_t, x_spd, y_spd, dead)
140
+ end
141
+
142
+ # read all outstanding async packets and store in async_responses
143
+ # would not do well to receive simple responses this way...
144
+ def read_async_messages
145
+ header, body = nil
146
+ new_responses = []
147
+
148
+ @lock.synchronize do
149
+ header, body = read_next_response
150
+
151
+ while header && Response.async?(header)
152
+ new_responses << Response::AsyncResponse.response(header, body)
153
+ header, body = read_next_response
154
+ end
155
+ end
156
+
157
+ async_messages.concat(new_responses) unless new_responses.empty?
158
+ return !new_responses.empty?
159
+ end
160
+
113
161
  private
114
162
 
115
163
  def is_windows?
@@ -122,24 +170,39 @@ class Sphero
122
170
  end
123
171
 
124
172
  def initialize_serialport dev
173
+ require 'serialport'
125
174
  @sp = SerialPort.new dev, 115200, 8, 1, SerialPort::NONE
126
175
  if is_windows?
127
176
  @sp.read_timeout=1000
128
177
  @sp.write_timeout=0
129
178
  @sp.initial_byte_offset=5
130
179
  end
180
+ rescue LoadError
181
+ puts "Please 'gem install hybridgroup-serialport' for serial port support."
131
182
  end
132
183
 
133
184
  def write packet
134
- header = nil
135
- body = nil
185
+ header, body = nil
136
186
 
137
187
  @lock.synchronize do
188
+ rs, ws = IO.select([], [@sp], [], 20)
138
189
  @sp.write packet.to_str
139
190
  @seq += 1
140
191
 
141
- header = @sp.read(5).unpack 'C5'
142
- body = @sp.read header.last
192
+ header = nil
193
+ loop do
194
+ header = read_header(true)
195
+ break if header
196
+ end
197
+
198
+ body = read_body(header.last, true) if header
199
+
200
+ # pick off asynch packets and store, till we get to the message response
201
+ while header && Response.async?(header)
202
+ async_messages << Response::AsyncResponse.response(header, body)
203
+ header = read_header(true)
204
+ body = read_body(header.last, true) if header
205
+ end
143
206
  end
144
207
 
145
208
  response = packet.response header, body
@@ -147,8 +210,58 @@ class Sphero
147
210
  if response.success?
148
211
  response
149
212
  else
150
- raise response
213
+ raise "Unable to write to Sphero!"
151
214
  end
152
215
  end
216
+
217
+ def read_header(blocking=false)
218
+ begin
219
+ data = read_next_chunk(5, blocking)
220
+ return nil unless data && data.length == 5
221
+ header = data.unpack 'C5'
222
+ rescue Errno::EBUSY
223
+ retry
224
+ rescue Exception => e
225
+ puts e.message
226
+ puts e.backtrace.inspect
227
+ retry
228
+ end
229
+
230
+ header
231
+ end
232
+
233
+ def read_body(len, blocking=false)
234
+ begin
235
+ data = read_next_chunk(len, blocking)
236
+ return nil unless data && data.length == len
237
+ rescue Errno::EBUSY
238
+ retry
239
+ rescue Exception => e
240
+ puts e.message
241
+ puts e.backtrace.inspect
242
+ retry
243
+ end
244
+
245
+ data
246
+ end
247
+
248
+ def read_next_chunk(len, blocking=false)
249
+ begin
250
+ if blocking || is_windows?
251
+ rs, ws, = IO.select([@sp], [], [], 20)
252
+ data = @sp.read(len)
253
+ return nil unless data && data.length == len
254
+ else
255
+ data = @sp.read_nonblock(len)
256
+ end
257
+ rescue Errno::EBUSY
258
+ retry
259
+ rescue Exception => e
260
+ puts e.message
261
+ puts e.backtrace.inspect
262
+ return nil
263
+ end
264
+ data
265
+ end
153
266
  end
154
267
 
data/test/test_sphero.rb CHANGED
@@ -3,11 +3,12 @@ require 'sphero'
3
3
  require 'mocha/setup'
4
4
 
5
5
  class TestSphero < MiniTest::Unit::TestCase
6
- def setup
7
- Sphero.any_instance.stubs(:initialize_serialport)
8
- @sphero = Sphero.new 'port123'
9
- @seq = 0x00
10
- end
6
+ def setup
7
+ Sphero.any_instance.stubs(:initialize_serialport)
8
+ Sphero.any_instance.stubs(:initialize_socket)
9
+ @sphero = Sphero.new 'port123'
10
+ @seq = 0x00
11
+ end
11
12
 
12
13
  def test_start_returns_new_sphero
13
14
  assert_kind_of Sphero, Sphero.start('someport')
@@ -22,77 +23,77 @@ class TestSphero < MiniTest::Unit::TestCase
22
23
  end
23
24
 
24
25
  def test_ping
25
- Sphero::Request::Ping.expects(:new).with(@seq)
26
- @sphero.expects(:write)
27
- @sphero.ping
26
+ Sphero::Request::Ping.expects(:new).with(@seq)
27
+ @sphero.expects(:write)
28
+ @sphero.ping
28
29
  end
29
30
 
30
31
  def test_version
31
- Sphero::Request::GetVersioning.expects(:new).with(@seq)
32
- @sphero.expects(:write)
33
- @sphero.version
32
+ Sphero::Request::GetVersioning.expects(:new).with(@seq)
33
+ @sphero.expects(:write)
34
+ @sphero.version
34
35
  end
35
36
 
36
37
  def test_bluetooth_info
37
- Sphero::Request::GetBluetoothInfo.expects(:new).with(@seq)
38
- @sphero.expects(:write)
39
- @sphero.bluetooth_info
38
+ Sphero::Request::GetBluetoothInfo.expects(:new).with(@seq)
39
+ @sphero.expects(:write)
40
+ @sphero.bluetooth_info
40
41
  end
41
42
 
42
43
  def test_auto_reconnect=
43
- @time_s = "10"
44
- Sphero::Request::SetAutoReconnect.expects(:new).with(@seq, @time_s)
45
- @sphero.expects(:write)
46
- @sphero.auto_reconnect = @time_s
44
+ @time_s = "10"
45
+ Sphero::Request::SetAutoReconnect.expects(:new).with(@seq, @time_s)
46
+ @sphero.expects(:write)
47
+ @sphero.auto_reconnect = @time_s
47
48
  end
48
49
 
49
50
  def test_auto_reconnect
50
- @time_s = "10"
51
- packet = mock 'packet'
52
- packet.stubs(:time).returns(@time_s)
51
+ @time_s = "10"
52
+ packet = mock 'packet'
53
+ packet.stubs(:time).returns(@time_s)
53
54
 
54
- Sphero::Request::GetAutoReconnect.expects(:new).with(@seq)
55
- @sphero.expects(:write).returns(packet)
56
- assert_equal @sphero.auto_reconnect, @time_s
55
+ Sphero::Request::GetAutoReconnect.expects(:new).with(@seq)
56
+ @sphero.expects(:write).returns(packet)
57
+ assert_equal @sphero.auto_reconnect, @time_s
57
58
  end
58
59
 
59
60
  def test_disable_auto_reconnect
60
- Sphero::Request::SetAutoReconnect.expects(:new).with(@seq, 0, 0x00)
61
- @sphero.expects(:write)
62
- @sphero.disable_auto_reconnect
61
+ Sphero::Request::SetAutoReconnect.expects(:new).with(@seq, 0, 0x00)
62
+ @sphero.expects(:write)
63
+ @sphero.disable_auto_reconnect
63
64
  end
64
65
 
65
66
  def test_power_state
66
- Sphero::Request::GetPowerState.expects(:new).with(@seq)
67
- @sphero.expects(:write)
68
- @sphero.power_state
67
+ Sphero::Request::GetPowerState.expects(:new).with(@seq)
68
+ @sphero.expects(:write)
69
+ @sphero.power_state
69
70
  end
70
71
 
71
72
  def test_sphero_sleep
72
- wakeup = 1
73
- macro = 2
74
- Sphero::Request::Sleep.expects(:new).with(@seq, wakeup, macro)
75
- @sphero.expects(:write)
76
- @sphero.sphero_sleep wakeup, macro
73
+ wakeup = 1
74
+ macro = 2
75
+ Sphero::Request::Sleep.expects(:new).with(@seq, wakeup, macro)
76
+ @sphero.expects(:write)
77
+ @sphero.sphero_sleep wakeup, macro
77
78
  end
78
79
 
79
80
  def test_roll
80
- speed = 1
81
- heading = 2
82
- state = 1
83
- Sphero::Request::Roll.expects(:new).with(@seq, speed, heading, state)
84
- @sphero.expects(:write)
85
- @sphero.roll speed, heading, true
81
+ speed = 1
82
+ heading = 2
83
+ state = 1
84
+ Sphero::Request::Roll.expects(:new).with(@seq, speed, heading, state)
85
+ @sphero.expects(:write)
86
+ @sphero.roll speed, heading, true
86
87
  end
87
88
 
88
89
  def test_stop
89
- @sphero.expects(:roll).with(0, 0)
90
- @sphero.stop
90
+ @sphero.expects(:roll).with(0, 0)
91
+ @sphero.stop
91
92
  end
92
93
 
93
94
  def test_keepgoing
94
- Kernel.expects(:sleep).with(3)
95
- @sphero.keep_going 3
95
+ Kernel.expects(:sleep).with(3)
96
+ @sphero.keep_going 3
96
97
  end
97
98
 
98
99
  def test_directions
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hybridgroup-sphero
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.6
4
+ version: 1.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,24 +9,8 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-14 00:00:00.000000000 Z
12
+ date: 2013-02-23 00:00:00.000000000 Z
13
13
  dependencies:
14
- - !ruby/object:Gem::Dependency
15
- name: hybridgroup-serialport
16
- requirement: !ruby/object:Gem::Requirement
17
- none: false
18
- requirements:
19
- - - ! '>='
20
- - !ruby/object:Gem::Version
21
- version: '0'
22
- type: :runtime
23
- prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ! '>='
28
- - !ruby/object:Gem::Version
29
- version: '0'
30
14
  - !ruby/object:Gem::Dependency
31
15
  name: minitest
32
16
  requirement: !ruby/object:Gem::Requirement