Nabaztag 0.2.0

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 (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: []