Nabaztag 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/CHANGES +13 -0
  2. data/README +22 -0
  3. data/bin/nabaztag-say +78 -0
  4. data/lib/nabaztag.rb +384 -0
  5. metadata +45 -0
data/CHANGES ADDED
@@ -0,0 +1,13 @@
1
+ = Changes
2
+
3
+ == 0.2.0
4
+
5
+ * Version 2 API support
6
+ * Can speak French or English
7
+ * Added command-line utility, nabaztag-say
8
+ * Added gem
9
+
10
+ == 0.1
11
+
12
+ * Initial release
13
+ * Version 1 API only
data/README ADDED
@@ -0,0 +1,22 @@
1
+ = Nabaztag
2
+
3
+ == About
4
+
5
+ Nabaztag communication library for Ruby.
6
+
7
+ The Nabaztag is a small electronic rabbit with lights, motorised ears, and speech.
8
+
9
+ http://www.nabaztag.com/
10
+
11
+ == Copying
12
+
13
+ Copyright Revieworld Ltd. 2006
14
+
15
+ You may use, copy and redistribute this library under the same terms as Ruby itself
16
+ (http://www.ruby-lang.org/en/LICENSE.txt).
17
+
18
+ == Installation
19
+
20
+ To install (requires root/admin privileges):
21
+
22
+ # ruby setup.rb
data/bin/nabaztag-say ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'nabaztag'
4
+ require 'optparse'
5
+ load ENV['HOME'] + '/.nabaztag' rescue nil
6
+
7
+ OPTIONS = {
8
+ :mac => NABAZTAG_MAC,
9
+ :token => NABAZTAG_TOKEN,
10
+ :voice => Nabaztag::VOICES[:en].first
11
+ }
12
+
13
+ ARGV.options do |o|
14
+ script_name = File.basename($0)
15
+
16
+ o.set_summary_indent(' ')
17
+ o.banner = "Usage: #{script_name} [OPTIONS] TEXT-TO-SAY"
18
+ o.define_head "Tell Nabaztag to say something."
19
+ o.separator ""
20
+ o.separator "If you create a file in your home directory called .nabaztag"
21
+ o.separator "containing the MAC and API token for your device, like this:"
22
+ o.separator " NABAZTAG_MAC = '00039XXXXXXX'"
23
+ o.separator " NABAZTAG_TOKEN = '1234XXXXXXXXXXX'"
24
+ o.separator "then you need not specify the MAC and token on the command line."
25
+ o.separator ""
26
+ o.separator "Options:"
27
+
28
+ begin
29
+ o.on(
30
+ "-m", "--mac=MAC", String,
31
+ "MAC address of device",
32
+ "Default is NABAZTAG_MAC in ~/.nabaztag"
33
+ ) { |OPTIONS[:mac]| }
34
+ o.on(
35
+ "-t", "--token=TOKEN", String,
36
+ "API token",
37
+ "Default is NABAZTAG_TOKEN in ~/.nabaztag"
38
+ ) { |OPTIONS[:token]| }
39
+ o.on(
40
+ "-v", "--voice=VOICE", String,
41
+ "Voice to use for speech",
42
+ "Default is #{OPTIONS[:voice]}"
43
+ ) { |OPTIONS[:voice]| }
44
+ o.on(
45
+ "-l", "--list-voices",
46
+ "List available voices and exit"
47
+ ) {
48
+ all = []
49
+ Nabaztag::VOICES.each do |lang, voices|
50
+ voices.each do |voice|
51
+ all << "#{voice} (#{lang})"
52
+ end
53
+ end
54
+ puts all.sort.join("\n")
55
+ exit
56
+ }
57
+ o.separator ""
58
+ o.on_tail("-h", "--help", "Show this help message.") { puts o; exit }
59
+ o.parse!
60
+
61
+ OPTIONS.each do |k,v|
62
+ if !v || v.empty?
63
+ raise RuntimeError, "Missing parameter #{v}"
64
+ end
65
+ end
66
+ if ARGV.empty?
67
+ raise RuntimeError, "Nothing to say"
68
+ end
69
+ rescue StandardError => e
70
+ puts "Error: #{e.message}", ''
71
+ puts o
72
+ exit 1
73
+ end
74
+ end
75
+
76
+ n = Nabaztag.new(OPTIONS[:mac], OPTIONS[:token])
77
+ n.voice = OPTIONS[:voice]
78
+ n.say!(ARGV.join(' '))
data/lib/nabaztag.rb ADDED
@@ -0,0 +1,384 @@
1
+ # This file is in UTF-8
2
+
3
+ require 'cgi'
4
+ require 'open-uri'
5
+ require 'iconv'
6
+
7
+ #
8
+ # Nabaztag allows control of the text-to-speech, ear control, and choreography features of
9
+ # Nabaztag devices.
10
+ #
11
+ # To use this library, you need to know the MAC of the device (written on the base) and its
12
+ # API token. The token must be obtained from http://www.nabaztag.com/vl/FR/api_prefs.jsp .
13
+ #
14
+ # The API allows different commands to be dispatched simultaneously; in order to achieve this,
15
+ # this library queues commands until they are sent.
16
+ #
17
+ # E.g.
18
+ # nabaztag = Nabaztag.new(mac, token)
19
+ # nabaztag.say('bonjour') # Nothing sent yet
20
+ # nabaztag.move_ears(4, 4) # Still not sent
21
+ # nabaztag.send # Messages sent
22
+ #
23
+ # This also means that if two conflicting commands are issued without an intervening send,
24
+ # only the latter will be carried out.
25
+ #
26
+ # However, beware! The API doesn't seem to respond well if multiple commands are sent in
27
+ # a short period of time: it can become confused and send erroneous commands to the device.
28
+ #
29
+ # In addition, the choreography command does not seem to play well with other commands: if
30
+ # text-to-speech and choreography are sent in one request, only the speech will get through
31
+ # to the rabbit.
32
+ #
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
36
+ # speak in English when told to use an English voice.
37
+ #
38
+ # The known voices are grouped by language in the Nabaztag::VOICES constant, but no attempt
39
+ # is made to validate against this list, as Violet may introduce additional voices in future.
40
+ #
41
+ class Nabaztag
42
+
43
+ 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
+
63
+ #
64
+ # The available voices for English and French according to the API. Note: although the French-language
65
+ # documentation lists the voices with leading capitals (e.g. Graham22s), the API only seems to
66
+ # recognise names all in lower case.
67
+ #
68
+ VOICES = {
69
+ :fr => %w[julie22k claire22s],
70
+ :en => %w[graham22s lucy22s heather22k ryan22k aaron22s laura22s]
71
+ }
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
+
97
+ #
98
+ # Create a new Nabaztag instance to communicate with the device with the given MAC address and
99
+ # service token (see class overview for explanation of token).
100
+ #
101
+ def initialize(mac, token)
102
+ @mac, @token = mac, token
103
+ @message = new_message
104
+ @ear_positions = [nil, nil]
105
+ end
106
+ attr_reader :mac, :token
107
+ attr_accessor :voice
108
+
109
+ #
110
+ # Send all pending messages
111
+ #
112
+ def send
113
+ response = @message.send
114
+ @message = new_message
115
+ return response
116
+ end
117
+ attr_reader :message
118
+
119
+ #
120
+ # Send a message immediately to get the ear positions.
121
+ #
122
+ def ear_positions
123
+ ear_message = new_message
124
+ ear_message.ears = 'ok'
125
+ ear_message.send
126
+ return ear_message.ear_positions
127
+ end
128
+
129
+ #
130
+ # Say text.
131
+ #
132
+ def say(text)
133
+ message.tts = text
134
+ message.verifiers["Speech"] = lambda{ |response| response =~ SUCCESS_RESPONSES[:say] }
135
+ nil
136
+ end
137
+
138
+ #
139
+ # Say text immediately.
140
+ #
141
+ def say!(text)
142
+ say(text)
143
+ send
144
+ end
145
+
146
+ #
147
+ # Make the rabbit bark.
148
+ #
149
+ def bark
150
+ say('ouah ouah')
151
+ nil
152
+ end
153
+
154
+ #
155
+ # Bark immediately.
156
+ #
157
+ def bark!
158
+ bark
159
+ send
160
+ end
161
+
162
+ #
163
+ # 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
165
+ # direction of movement. For more precise ear control, use choreography instead.
166
+ #
167
+ def move_ears(left, right)
168
+ message.posleft = left if left
169
+ 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
177
+ end
178
+
179
+ #
180
+ # Move ears immediately.
181
+ #
182
+ def move_ears!(left, right)
183
+ move_ears(left, right)
184
+ send
185
+ end
186
+
187
+ #
188
+ # Creates a new choreography message based on the actions instructed in the block. The commands
189
+ # are evaluated in the context of a new Choreography instance.
190
+ #
191
+ # E.g.
192
+ # nabaztag.choreography do
193
+ # event { led :middle, :green ; led :left, :red }
194
+ # led :right, :yellow
195
+ # event { led :left, :off ; led :right, :off}
196
+ # ...
197
+ # end
198
+ #
199
+ def choreography(title=nil, &blk)
200
+ 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] }
205
+ nil
206
+ end
207
+
208
+ #
209
+ # Creates choreography and sends it immediately.
210
+ #
211
+ def choreography!(title=nil, &blk)
212
+ choreography(title, &blk)
213
+ send
214
+ end
215
+
216
+ private
217
+
218
+ def new_message
219
+ return Message.new(self)
220
+ 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
+
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
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: Nabaztag
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.2.0
7
+ date: 2006-08-02 00:00:00 +01:00
8
+ summary: Nabaztag communication library for Ruby.
9
+ require_paths:
10
+ - lib
11
+ email: paulbattley@reevoo.com
12
+ homepage:
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: nabaztag
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ -
22
+ - ">"
23
+ - !ruby/object:Gem::Version
24
+ version: 0.0.0
25
+ version:
26
+ platform: ruby
27
+ signing_key:
28
+ cert_chain:
29
+ post_install_message:
30
+ authors:
31
+ - Paul Battley
32
+ files:
33
+ - lib/nabaztag.rb
34
+ - README
35
+ - CHANGES
36
+ test_files: []
37
+ rdoc_options: []
38
+ extra_rdoc_files:
39
+ - README
40
+ - CHANGES
41
+ executables:
42
+ - nabaztag-say
43
+ extensions: []
44
+ requirements: []
45
+ dependencies: []