Nabaztag 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,5 +1,10 @@
1
1
  = Changes
2
2
 
3
+ == 0.3.0
4
+
5
+ * Understands new XML responses from API
6
+ * Significant refactoring of response handling
7
+
3
8
  == 0.2.0
4
9
 
5
10
  * Version 2 API support
data/lib/nabaztag.rb CHANGED
@@ -1,14 +1,12 @@
1
- # This file is in UTF-8
2
-
3
- require 'cgi'
4
- require 'open-uri'
5
- require 'iconv'
1
+ require 'nabaztag/message'
2
+ require 'nabaztag/response'
3
+ require 'nabaztag/choreography'
6
4
 
7
5
  #
8
- # Nabaztag allows control of the text-to-speech, ear control, and choreography features of
6
+ # Nabaztag allows control of the text-to-speech, ear control, and choreography features of
9
7
  # Nabaztag devices.
10
8
  #
11
- # To use this library, you need to know the MAC of the device (written on the base) and its
9
+ # To use this library, you need to know the MAC of the device (written on the base) and its
12
10
  # API token. The token must be obtained from http://www.nabaztag.com/vl/FR/api_prefs.jsp .
13
11
  #
14
12
  # The API allows different commands to be dispatched simultaneously; in order to achieve this,
@@ -20,7 +18,7 @@ require 'iconv'
20
18
  # nabaztag.move_ears(4, 4) # Still not sent
21
19
  # nabaztag.send # Messages sent
22
20
  #
23
- # This also means that if two conflicting commands are issued without an intervening send,
21
+ # This also means that if two conflicting commands are issued without an intervening send,
24
22
  # only the latter will be carried out.
25
23
  #
26
24
  # However, beware! The API doesn't seem to respond well if multiple commands are sent in
@@ -30,38 +28,20 @@ require 'iconv'
30
28
  # text-to-speech and choreography are sent in one request, only the speech will get through
31
29
  # to the rabbit.
32
30
  #
33
- # With version 2 of the API, it is now possible to specify a voice for the message. The
34
- # default is determined by the rabbit's language (claire22s for French; heather22k for
35
- # English). The voice's language overrides that of the rabbit: i.e. a French rabbit will
31
+ # With version 2 of the API, it is now possible to specify a voice for the message. The
32
+ # default is determined by the rabbit's language (claire22s for French; heather22k for
33
+ # English). The voice's language overrides that of the rabbit: i.e. a French rabbit will
36
34
  # speak in English when told to use an English voice.
37
35
  #
38
- # The known voices are grouped by language in the Nabaztag::VOICES constant, but no attempt
36
+ # The known voices are grouped by language in the Nabaztag::VOICES constant, but no attempt
39
37
  # is made to validate against this list, as Violet may introduce additional voices in future.
40
38
  #
41
39
  class Nabaztag
42
-
40
+
43
41
  class ServiceError < RuntimeError ; end
44
-
45
- SERVICE_ENCODING = 'iso-8859-1'
46
- API_URI = 'http://www.nabaztag.com/vl/FR/api.jsp?'
47
-
48
- #
49
- # The messages that indicate successful reception of various commands. Francophone rabbits reply in French;
50
- # anglophone ones reply in English
51
- #
52
- SUCCESS_RESPONSES = {
53
- :say => /Votre texte a bien été transmis|Your text was forwarded/u,
54
- :left_ear => /Votre changement d'oreilles gauche a été transmis|Your left change of ears was transmitted/u,
55
- :right_ear => /Votre changement d'oreilles droit a été transmis|Your right change of ears was transmitted/u,
56
- :choreography => /Votre chorégraphie a bien été transmis|Your choreography was forwarded/u
57
- }
58
- EAR_POSITION_RESPONSES = {
59
- :left => /(?:Position gauche|Left position) = (-?\d+)/u,
60
- :right => /(?:Position droite|Right position) = (-?\d+)/u
61
- }
62
-
42
+
63
43
  #
64
- # The available voices for English and French according to the API. Note: although the French-language
44
+ # The available voices for English and French according to the API. Note: although the French-language
65
45
  # documentation lists the voices with leading capitals (e.g. Graham22s), the API only seems to
66
46
  # recognise names all in lower case.
67
47
  #
@@ -69,33 +49,12 @@ class Nabaztag
69
49
  :fr => %w[julie22k claire22s],
70
50
  :en => %w[graham22s lucy22s heather22k ryan22k aaron22s laura22s]
71
51
  }
72
-
73
- class <<self
74
-
75
- #
76
- # Override the default system encoding: use this if your program is not using UTF-8
77
- #
78
- def system_encoding=(encoding)
79
- @system_encoding = encoding
80
- end
81
-
82
- def system_encoding
83
- return @system_encoding || 'utf-8'
84
- end
85
-
86
- def encode_text(string)
87
- Iconv.iconv(SERVICE_ENCODING, system_encoding, string)[0]
88
- end
89
-
90
- def decode_response(string)
91
- # Responses are only used for verification, so the encoding should match the present file.
92
- Iconv.iconv('utf-8', SERVICE_ENCODING, string)[0]
93
- end
94
-
95
- end
96
-
52
+
53
+ attr_reader :mac, :token, :message
54
+ attr_accessor :voice
55
+
97
56
  #
98
- # Create a new Nabaztag instance to communicate with the device with the given MAC address and
57
+ # Create a new Nabaztag instance to communicate with the device with the given MAC address and
99
58
  # service token (see class overview for explanation of token).
100
59
  #
101
60
  def initialize(mac, token)
@@ -103,8 +62,6 @@ class Nabaztag
103
62
  @message = new_message
104
63
  @ear_positions = [nil, nil]
105
64
  end
106
- attr_reader :mac, :token
107
- attr_accessor :voice
108
65
 
109
66
  #
110
67
  # Send all pending messages
@@ -112,29 +69,30 @@ class Nabaztag
112
69
  def send
113
70
  response = @message.send
114
71
  @message = new_message
72
+ unless response.success?
73
+ raise ServiceError, response.raw
74
+ end
115
75
  return response
116
76
  end
117
- attr_reader :message
118
-
77
+
119
78
  #
120
79
  # Send a message immediately to get the ear positions.
121
80
  #
122
81
  def ear_positions
123
82
  ear_message = new_message
124
83
  ear_message.ears = 'ok'
125
- ear_message.send
126
- return ear_message.ear_positions
84
+ response = ear_message.send
85
+ return [response.left_ear, response.right_ear]
127
86
  end
128
-
87
+
129
88
  #
130
89
  # Say text.
131
90
  #
132
91
  def say(text)
133
92
  message.tts = text
134
- message.verifiers["Speech"] = lambda{ |response| response =~ SUCCESS_RESPONSES[:say] }
135
93
  nil
136
94
  end
137
-
95
+
138
96
  #
139
97
  # Say text immediately.
140
98
  #
@@ -142,7 +100,7 @@ class Nabaztag
142
100
  say(text)
143
101
  send
144
102
  end
145
-
103
+
146
104
  #
147
105
  # Make the rabbit bark.
148
106
  #
@@ -158,24 +116,18 @@ class Nabaztag
158
116
  bark
159
117
  send
160
118
  end
161
-
119
+
162
120
  #
163
121
  # Set the position of the left and right ears between 0 and 16. Use nil to avoid moving an ear.
164
- # Note that these positions are not given in degrees, and that it is not possible to specify the
122
+ # Note that these positions are not given in degrees, and that it is not possible to specify the
165
123
  # direction of movement. For more precise ear control, use choreography instead.
166
124
  #
167
125
  def move_ears(left, right)
168
126
  message.posleft = left if left
169
127
  message.posright = right if right
170
- if left
171
- message.verifiers["Left ear"] = lambda{ |response| response =~ SUCCESS_RESPONSES[:left_ear] }
172
- end
173
- if right
174
- message.verifiers["Right ear"] = lambda{ |response| response =~ SUCCESS_RESPONSES[:right_ear] }
175
- end
176
- return nil
128
+ nil
177
129
  end
178
-
130
+
179
131
  #
180
132
  # Move ears immediately.
181
133
  #
@@ -183,28 +135,25 @@ class Nabaztag
183
135
  move_ears(left, right)
184
136
  send
185
137
  end
186
-
138
+
187
139
  #
188
- # Creates a new choreography message based on the actions instructed in the block. The commands
140
+ # Creates a new choreography message based on the actions instructed in the block. The commands
189
141
  # are evaluated in the context of a new Choreography instance.
190
142
  #
191
143
  # E.g.
192
144
  # nabaztag.choreography do
193
- # event { led :middle, :green ; led :left, :red }
145
+ # together { led :middle, :green ; led :left, :red }
194
146
  # led :right, :yellow
195
- # event { led :left, :off ; led :right, :off}
147
+ # together { led :left, :off ; led :right, :off}
196
148
  # ...
197
149
  # end
198
150
  #
199
151
  def choreography(title=nil, &blk)
200
152
  message.chortitle = title
201
- obj = Choreography.new
202
- obj.instance_eval(&blk)
203
- message.chor = obj.emit
204
- message.verifiers["Choreography"] = lambda{ |response| response =~ SUCCESS_RESPONSES[:choreography] }
153
+ message.chor = Choreography.new(&blk).build
205
154
  nil
206
155
  end
207
-
156
+
208
157
  #
209
158
  # Creates choreography and sends it immediately.
210
159
  #
@@ -212,173 +161,11 @@ class Nabaztag
212
161
  choreography(title, &blk)
213
162
  send
214
163
  end
215
-
216
- private
217
-
164
+
165
+ private
166
+
218
167
  def new_message
219
- return Message.new(self)
168
+ return Message.new(mac, token)
220
169
  end
221
-
222
- #
223
- # Choreography class uses class methods to implement a simple DSL. These build API choreography
224
- # messages based on instructions to move the ears and light the LEDs.
225
- #
226
- class Choreography
227
-
228
- LED_COLORS = {
229
- :red => [255, 0, 0],
230
- :orange => [255, 127, 0],
231
- :yellow => [255, 255, 0],
232
- :green => [ 0, 255, 0],
233
- :blue => [ 0, 0, 255],
234
- :purple => [255, 0, 255],
235
- :dim_red => [127, 0, 0],
236
- :dim_orange => [127, 63, 0],
237
- :dim_yellow => [127, 127, 0],
238
- :dim_green => [ 0, 127, 0],
239
- :dim_blue => [ 0, 0, 127],
240
- :dim_purple => [127, 0, 127],
241
- :off => [ 0, 0, 0]
242
- }
243
- EARS = {:left => [1], :right => [0], :both => [0,1]}
244
- LEDS = {:bottom => 0, :left => 1, :middle => 2, :right => 3, :top => 4}
245
- EAR_DIRECTIONS = {:forward => 0, :backward => 1}
246
-
247
- def emit
248
- @messages ||= []
249
- return (['%d' % (@tempo || 10)] + (@messages || []) ).join(',')
250
- end
251
-
252
- #
253
- # Set the tempo of the choreography in Hz (i.e. events per secod). The default is 10
254
- # events per second.
255
- #
256
- def tempo(t)
257
- @tempo = t
258
- end
259
-
260
- #
261
- # Move :left, :right, or :both ears to angle degrees (0-180) in direction
262
- # :forward (default) or :backward.
263
- #
264
- def ear(which_ear, angle, direction=:forward)
265
- direction_number = EAR_DIRECTIONS[direction]
266
- EARS[which_ear].each do |ear_number|
267
- append_message('motor', ear_number, angle, 0, direction)
268
- end
269
- skip 1
270
- end
271
-
272
- #
273
- # Change colour of an led (:top, :right:, middle, :left, :bottom) to a specified colour.
274
- # The colour may be specified either as RGB values (0-255) or by using one of the named colours
275
- # in LED_COLORS.
276
- #
277
- # E.g.
278
- # led :middle, :red
279
- # led :top, 0, 0, 255
280
- # led :bottom, :off
281
- #
282
- def led(which_led, c1, c2=nil, c3=nil)
283
- led_number = LEDS[which_led]
284
- if (c1 && c2 && c3)
285
- red, green, blue = c1, c2, c3
286
- else
287
- red, green, blue = LED_COLORS[c1]
288
- end
289
- append_message('led', led_number, red, green, blue)
290
- skip 1
291
- end
292
-
293
- #
294
- # Group several actions into a single chronological step via a block.
295
- #
296
- # E.g.
297
- # event { led :top, :yellow ; ear :both, 0 }
298
- #
299
- def event(&blk)
300
- length(1, &blk)
301
- end
302
-
303
- #
304
- # Perform one or more actions for n chronological steps
305
- #
306
- # E.g.
307
- # length 3 do
308
- # led :top, :red ; led :middle, :yellow
309
- # end
310
- #
311
- def length(duration, &blk)
312
- old_in_event = @in_event
313
- @in_event = true
314
- yield
315
- @in_event = old_in_event
316
- skip duration
317
- end
318
-
319
- private
320
-
321
- def append_message(*params)
322
- fields = [@time_stamp || 0] + params
323
- (@messages ||= []) << ("%d,%s,%d,%d,%d,%d" % fields)
324
- end
325
170
 
326
- def skip(duration=1)
327
- @time_stamp ||= 0
328
- @time_stamp += duration unless @in_event
329
- end
330
-
331
- end # Choreography
332
-
333
- class Message
334
-
335
- FIELDS = [:idmessage, :posright, :posleft, :idapp, :tts, :chor, :chortitle, :nabcast, :ears]
336
- FIELDS.each do |field|
337
- attr_accessor field
338
- end
339
-
340
- def initialize(nabaztag)
341
- @nabaztag = nabaztag
342
- @verifiers = {}
343
- end
344
- attr_reader :verifiers, :ear_positions
345
-
346
- def send
347
- parameters = FIELDS.inject({
348
- :sn => @nabaztag.mac,
349
- :token => @nabaztag.token,
350
- :voice => @nabaztag.voice
351
- }){ |hash, element|
352
- value = __send__(element)
353
- hash[element] = value if value
354
- hash
355
- }
356
- request = build_request(parameters)
357
- response = Nabaztag.decode_response(open(request).read).split(/\s{2,}/m).join("\n")
358
- decode_ear_positions(response) if @ears
359
- verifiers.each do |name, verifier|
360
- unless verifier.call(response)
361
- raise ServiceError, "#{name}: #{response}"
362
- end
363
- end
364
- return true
365
- end
366
-
367
- def decode_ear_positions(response)
368
- left_ear = response[EAR_POSITION_RESPONSES[:left], 1]
369
- right_ear = response[EAR_POSITION_RESPONSES[:right], 1]
370
- @ear_positions = [left_ear.to_i, right_ear.to_i] if left_ear && right_ear
371
- end
372
-
373
- private
374
-
375
- def build_request(parameters)
376
- return API_URI << parameters.map{ |k,v|
377
- value = CGI.escape(Nabaztag.encode_text(v.to_s))
378
- "#{k}=#{value}"
379
- }.join('&')
380
- end
381
-
382
- end # Message
383
-
384
- end # Nabaztag
171
+ end
@@ -0,0 +1,119 @@
1
+ class Nabaztag
2
+
3
+ #
4
+ # The Choreography class uses class methods to implement a simple DSL. These build API choreography
5
+ # messages based on instructions to move the ears and light the LEDs.
6
+ #
7
+ class Choreography
8
+
9
+ LED_COLORS = {
10
+ :red => [255, 0, 0],
11
+ :orange => [255, 127, 0],
12
+ :yellow => [255, 255, 0],
13
+ :green => [ 0, 255, 0],
14
+ :blue => [ 0, 0, 255],
15
+ :purple => [255, 0, 255],
16
+ :dim_red => [127, 0, 0],
17
+ :dim_orange => [127, 63, 0],
18
+ :dim_yellow => [127, 127, 0],
19
+ :dim_green => [ 0, 127, 0],
20
+ :dim_blue => [ 0, 0, 127],
21
+ :dim_purple => [127, 0, 127],
22
+ :off => [ 0, 0, 0]
23
+ }
24
+ EARS = {:left => [1], :right => [0], :both => [0,1]}
25
+ LEDS = {:bottom => 0, :left => 1, :middle => 2, :right => 3, :top => 4}
26
+ EAR_DIRECTIONS = {:forward => 0, :backward => 1}
27
+
28
+ def initialize(&blk)
29
+ @messages = []
30
+ @tempo = 10
31
+ @time_stamp = 0
32
+ instance_eval(&blk) if block_given?
33
+ end
34
+
35
+ def build
36
+ return (['%d' % @tempo] + @messages).join(',')
37
+ end
38
+
39
+ #
40
+ # Set the tempo of the choreography in Hz (i.e. events per secod). The default is 10
41
+ # events per second.
42
+ #
43
+ def tempo(t)
44
+ @tempo = t
45
+ end
46
+
47
+ #
48
+ # Move :left, :right, or :both ears to angle degrees (0-180) in direction
49
+ # :forward (default) or :backward.
50
+ #
51
+ def ear(which_ear, angle, direction=:forward)
52
+ direction_number = EAR_DIRECTIONS[direction]
53
+ EARS[which_ear].each do |ear_number|
54
+ append_message('motor', ear_number, angle, 0, direction_number)
55
+ end
56
+ advance
57
+ end
58
+
59
+ #
60
+ # Change colour of an led (:top, :right:, middle, :left, :bottom) to a specified colour.
61
+ # The colour may be specified either as RGB values (0-255) or by using one of the named colours
62
+ # in LED_COLORS.
63
+ #
64
+ # E.g.
65
+ # led :middle, :red
66
+ # led :top, 0, 0, 255
67
+ # led :bottom, :off
68
+ #
69
+ def led(which_led, c1, c2=nil, c3=nil)
70
+ led_number = LEDS[which_led]
71
+ if (c1 && c2 && c3)
72
+ red, green, blue = c1, c2, c3
73
+ else
74
+ red, green, blue = LED_COLORS[c1]
75
+ end
76
+ append_message('led', led_number, red, green, blue)
77
+ advance
78
+ end
79
+
80
+ #
81
+ # Group several actions into a single chronological step via a block.
82
+ #
83
+ # E.g.
84
+ # event { led :top, :yellow ; ear :both, 0 }
85
+ #
86
+ def event(duration=1, &blk)
87
+ length(duration, &blk)
88
+ end
89
+
90
+ alias_method :together, :event
91
+
92
+ #
93
+ # Perform one or more actions for n chronological steps
94
+ #
95
+ # E.g.
96
+ # length 3 do
97
+ # led :top, :red ; led :middle, :yellow
98
+ # end
99
+ #
100
+ def length(duration, &blk)
101
+ old_in_event = @in_event
102
+ @in_event = true
103
+ yield
104
+ @in_event = old_in_event
105
+ advance duration
106
+ end
107
+
108
+ private
109
+
110
+ def append_message(*params)
111
+ @messages << ("%d,%s,%d,%d,%d,%d" % ([@time_stamp] + params))
112
+ end
113
+
114
+ def advance(duration=1)
115
+ @time_stamp += duration unless @in_event
116
+ end
117
+
118
+ end
119
+ end
@@ -0,0 +1,58 @@
1
+ require 'cgi'
2
+ require 'iconv'
3
+ require 'open-uri'
4
+ require 'nabaztag/response'
5
+
6
+ class Nabaztag
7
+ class Message
8
+
9
+ SERVICE_ENCODING = 'iso-8859-1'
10
+ API_URI = 'http://api.nabaztag.com/vl/FR/api.jsp?'
11
+ FIELDS = [
12
+ :idmessage, :posright, :posleft, :idapp, :tts, :chor, :chortitle, :ears, :nabcast,
13
+ :ttlive, :voice, :speed, :pitch
14
+ ]
15
+
16
+ FIELDS.each do |field|
17
+ attr_accessor field
18
+ end
19
+
20
+ def initialize(mac, token)
21
+ @mac, @token = mac, token
22
+ @expected_identifiers = []
23
+ end
24
+
25
+ def send
26
+ Response.new(open(request_uri){ |io| io.read })
27
+ end
28
+
29
+ def expect(identifier)
30
+ @expected_identifiers << identifier
31
+ end
32
+
33
+ private
34
+
35
+ def request_uri
36
+ API_URI + parameters.sort_by{ |k,v| k.to_s }.map{ |k,v|
37
+ value = CGI.escape(encode_text(v.to_s))
38
+ "#{k}=#{value}"
39
+ }.join('&')
40
+ end
41
+
42
+ def parameters
43
+ FIELDS.inject({
44
+ :sn => @mac,
45
+ :token => @token
46
+ }){ |hash, element|
47
+ value = __send__(element)
48
+ hash[element] = value if value
49
+ hash
50
+ }
51
+ end
52
+
53
+ def encode_text(string)
54
+ Iconv.iconv(SERVICE_ENCODING, 'utf-8', string)[0]
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,55 @@
1
+ require 'rexml/document'
2
+
3
+ class Nabaztag
4
+ class Response
5
+ include REXML
6
+
7
+ ERROR_MESSAGES = %w[
8
+ NOGOODTOKENORSERIAL
9
+ NOTAVAILABLE
10
+ ]
11
+ SUCCESS_MESSAGES = %w[
12
+ TTSEND
13
+ EARPOSITIONSEND
14
+ POSITIONEAR
15
+ MESSAGESEND
16
+ ]
17
+ # As at 2007-03-07, choreography and nabcast always return NOTAVAILABLE
18
+
19
+ attr_reader :raw
20
+
21
+ def initialize(xml)
22
+ @raw = xml
23
+ @doc = Document.new(xml)
24
+ end
25
+
26
+ def messages
27
+ lookup('/rsp/message')
28
+ end
29
+
30
+ def comments
31
+ lookup('/rsp/comment')
32
+ end
33
+
34
+ def left_ear
35
+ position = lookup('/rsp/leftposition').first
36
+ position && position.to_i
37
+ end
38
+
39
+ def right_ear
40
+ position = lookup('/rsp/rightposition').first
41
+ position && position.to_i
42
+ end
43
+
44
+ def success?
45
+ (messages & ERROR_MESSAGES).empty?
46
+ end
47
+
48
+ private
49
+
50
+ def lookup(xpath)
51
+ XPath.match(@doc, xpath).map{ |n| n.text }
52
+ end
53
+
54
+ end
55
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.9.0
2
+ rubygems_version: 0.9.1
3
3
  specification_version: 1
4
4
  name: Nabaztag
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.2.0
7
- date: 2006-08-02 00:00:00 +01:00
6
+ version: 0.3.0
7
+ date: 2007-04-25 00:00:00 +01:00
8
8
  summary: Nabaztag communication library for Ruby.
9
9
  require_paths:
10
- - lib
10
+ - lib
11
11
  email: paulbattley@reevoo.com
12
12
  homepage:
13
13
  rubyforge_project:
@@ -18,28 +18,35 @@ bindir: bin
18
18
  has_rdoc: true
19
19
  required_ruby_version: !ruby/object:Gem::Version::Requirement
20
20
  requirements:
21
- -
22
- - ">"
23
- - !ruby/object:Gem::Version
24
- version: 0.0.0
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
25
24
  version:
26
25
  platform: ruby
27
26
  signing_key:
28
27
  cert_chain:
29
28
  post_install_message:
30
29
  authors:
31
- - Paul Battley
30
+ - Paul Battley
32
31
  files:
33
- - lib/nabaztag.rb
34
- - README
35
- - CHANGES
32
+ - lib/nabaztag.rb
33
+ - lib/nabaztag/choreography.rb
34
+ - lib/nabaztag/message.rb
35
+ - lib/nabaztag/response.rb
36
+ - README
37
+ - CHANGES
36
38
  test_files: []
39
+
37
40
  rdoc_options: []
41
+
38
42
  extra_rdoc_files:
39
- - README
40
- - CHANGES
43
+ - README
44
+ - CHANGES
41
45
  executables:
42
- - nabaztag-say
46
+ - nabaztag-say
43
47
  extensions: []
48
+
44
49
  requirements: []
45
- dependencies: []
50
+
51
+ dependencies: []
52
+