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.
- data/CHANGES +13 -0
- data/README +22 -0
- data/bin/nabaztag-say +78 -0
- data/lib/nabaztag.rb +384 -0
- metadata +45 -0
data/CHANGES
ADDED
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: []
|