artoo 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +25 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +144 -0
- data/Guardfile +15 -0
- data/LICENSE +13 -0
- data/README.md +97 -0
- data/Rakefile +11 -0
- data/api/assets/compass.rb +25 -0
- data/api/assets/javascripts/artoo/controllers/robot.js.coffee +27 -0
- data/api/assets/javascripts/artoo/routes.js.coffee +23 -0
- data/api/assets/javascripts/core.js.coffee +6 -0
- data/api/assets/javascripts/vendor/angular.min.js +161 -0
- data/api/assets/javascripts/vendor/bootstrap.min.js +6 -0
- data/api/assets/javascripts/vendor/jquery.min.js +5 -0
- data/api/assets/stylesheets/artoo/_core.css.scss +3 -0
- data/api/assets/stylesheets/artoo/_font-awesome.scss +534 -0
- data/api/assets/stylesheets/artoo/_variables.scss +8 -0
- data/api/assets/stylesheets/core.scss +70 -0
- data/api/public/core.css +9848 -0
- data/api/public/core.js +259 -0
- data/api/public/favicon.ico +0 -0
- data/api/public/font/FontAwesome.otf +0 -0
- data/api/public/font/fontawesome-webfont.eot +0 -0
- data/api/public/font/fontawesome-webfont.svg +284 -0
- data/api/public/font/fontawesome-webfont.ttf +0 -0
- data/api/public/font/fontawesome-webfont.woff +0 -0
- data/api/public/html5shiv.js +8 -0
- data/api/public/images/devices/ardrone.jpg +0 -0
- data/api/public/images/devices/arduino.jpg +0 -0
- data/api/public/images/devices/sphero.png +0 -0
- data/api/public/images/glyphicons-halflings-white.png +0 -0
- data/api/public/images/glyphicons-halflings.png +0 -0
- data/api/public/index.html +36 -0
- data/api/public/partials/robot-detail.html +111 -0
- data/api/public/partials/robot-device-detail.html +0 -0
- data/api/public/partials/robot-index.html +26 -0
- data/artoo.gemspec +21 -0
- data/bin/retry.sh +8 -0
- data/bin/sphero.sh +8 -0
- data/examples/ardrone.rb +12 -0
- data/examples/ardrone_nav.rb +22 -0
- data/examples/ardrone_nav_video.rb +33 -0
- data/examples/ardrone_video.rb +22 -0
- data/examples/conway_sphero.rb +65 -0
- data/examples/firmata.rb +13 -0
- data/examples/firmata_button.rb +9 -0
- data/examples/hello.rb +12 -0
- data/examples/hello_api.rb +9 -0
- data/examples/hello_api_multiple.rb +25 -0
- data/examples/hello_modular.rb +16 -0
- data/examples/hello_multiple.rb +22 -0
- data/examples/notifications.rb +9 -0
- data/examples/sphero.rb +11 -0
- data/examples/sphero_color.rb +13 -0
- data/examples/sphero_firmata.rb +17 -0
- data/examples/sphero_messages.rb +22 -0
- data/examples/sphero_multiple.rb +33 -0
- data/examples/wiiclassic.rb +94 -0
- data/lib/artoo.rb +3 -0
- data/lib/artoo/adaptors/adaptor.rb +54 -0
- data/lib/artoo/adaptors/ardrone.rb +32 -0
- data/lib/artoo/adaptors/ardrone_navigation.rb +26 -0
- data/lib/artoo/adaptors/ardrone_video.rb +27 -0
- data/lib/artoo/adaptors/firmata.rb +25 -0
- data/lib/artoo/adaptors/loopback.rb +8 -0
- data/lib/artoo/adaptors/sphero.rb +46 -0
- data/lib/artoo/api.rb +48 -0
- data/lib/artoo/api_route_helpers.rb +197 -0
- data/lib/artoo/connection.rb +70 -0
- data/lib/artoo/delegator.rb +49 -0
- data/lib/artoo/device.rb +61 -0
- data/lib/artoo/device_event_client.rb +27 -0
- data/lib/artoo/drivers/ardrone.rb +9 -0
- data/lib/artoo/drivers/ardrone_navigation.rb +21 -0
- data/lib/artoo/drivers/ardrone_video.rb +22 -0
- data/lib/artoo/drivers/button.rb +40 -0
- data/lib/artoo/drivers/driver.rb +48 -0
- data/lib/artoo/drivers/led.rb +37 -0
- data/lib/artoo/drivers/passthru.rb +9 -0
- data/lib/artoo/drivers/pinger.rb +19 -0
- data/lib/artoo/drivers/pinger2.rb +18 -0
- data/lib/artoo/drivers/sphero.rb +57 -0
- data/lib/artoo/drivers/wiichuck.rb +29 -0
- data/lib/artoo/drivers/wiiclassic.rb +137 -0
- data/lib/artoo/main.rb +32 -0
- data/lib/artoo/master.rb +16 -0
- data/lib/artoo/port.rb +51 -0
- data/lib/artoo/robot.rb +299 -0
- data/lib/artoo/utility.rb +39 -0
- data/lib/artoo/version.rb +5 -0
- data/test/adaptors/adaptor_test.rb +18 -0
- data/test/adaptors/ardrone_test.rb +24 -0
- data/test/adaptors/firmata_test.rb +25 -0
- data/test/adaptors/loopback_test.rb +18 -0
- data/test/adaptors/sphero_test.rb +24 -0
- data/test/api_test.rb +61 -0
- data/test/artoo_test.rb +12 -0
- data/test/connection_test.rb +28 -0
- data/test/delegator_test.rb +71 -0
- data/test/device_test.rb +41 -0
- data/test/drivers/ardrone_navigation_test.rb +11 -0
- data/test/drivers/ardrone_test.rb +11 -0
- data/test/drivers/ardrone_video_test.rb +11 -0
- data/test/drivers/driver_test.rb +21 -0
- data/test/drivers/led_test.rb +52 -0
- data/test/drivers/sphero_test.rb +54 -0
- data/test/drivers/wiichuck_test.rb +11 -0
- data/test/port_test.rb +33 -0
- data/test/robot_test.rb +96 -0
- data/test/test_helper.rb +8 -0
- data/test/utility_test.rb +27 -0
- metadata +185 -0
data/lib/artoo/main.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'artoo/delegator'
|
2
|
+
require 'artoo/robot'
|
3
|
+
|
4
|
+
module Artoo
|
5
|
+
# Execution context for top-level robots
|
6
|
+
# DSL methods executed on main are delegated to this class like Sinatra
|
7
|
+
class MainRobot < Artoo::Robot
|
8
|
+
|
9
|
+
# we assume that the first file that requires 'artoo' is the
|
10
|
+
# app_file. all other path related options are calculated based
|
11
|
+
# on this path by default.
|
12
|
+
set :app_file, caller_files.first || $0
|
13
|
+
set :start_work, Proc.new { File.expand_path($0) == File.expand_path(app_file) }
|
14
|
+
|
15
|
+
# if run? && ARGV.any?
|
16
|
+
# require 'optparse'
|
17
|
+
# OptionParser.new { |op|
|
18
|
+
# op.on('-p port', 'set the port (default is 4567)') { |val| set :port, Integer(val) }
|
19
|
+
# op.on('-o addr', 'set the host (default is 0.0.0.0)') { |val| set :bind, val }
|
20
|
+
# op.on('-e env', 'set the environment (default is development)') { |val| set :environment, val.to_sym }
|
21
|
+
# op.on('-s server', 'specify rack server/handler (default is thin)') { |val| set :server, val }
|
22
|
+
# op.on('-x', 'turn on the mutex lock (default is off)') { set :lock, true }
|
23
|
+
# }.parse!(ARGV.dup)
|
24
|
+
# end
|
25
|
+
end
|
26
|
+
|
27
|
+
at_exit { MainRobot.work! if $!.nil? && MainRobot.start_work? }
|
28
|
+
end
|
29
|
+
|
30
|
+
# include would include the module in Object
|
31
|
+
# extend only extends the `main` object
|
32
|
+
extend Artoo::Delegator
|
data/lib/artoo/master.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Artoo
|
2
|
+
# The Artoo::Master class is a registered supervisor class to keep track
|
3
|
+
# of all the running robots
|
4
|
+
class Master
|
5
|
+
include Celluloid
|
6
|
+
attr_reader :robots
|
7
|
+
|
8
|
+
def initialize(bots)
|
9
|
+
@robots = bots
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_robot_by_name(name)
|
13
|
+
robots.find_all {|r| r.name == name}.first
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/artoo/port.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Artoo
|
2
|
+
# The Artoo::Port class represents port and/or host to be used to connect
|
3
|
+
# tp a specific individual hardware device.
|
4
|
+
class Port
|
5
|
+
attr_reader :port, :host
|
6
|
+
|
7
|
+
def initialize(data)
|
8
|
+
@is_tcp, @is_serial = false
|
9
|
+
parse(data)
|
10
|
+
end
|
11
|
+
|
12
|
+
def is_serial?
|
13
|
+
@is_serial == true
|
14
|
+
end
|
15
|
+
|
16
|
+
def is_tcp?
|
17
|
+
@is_tcp == true
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
if is_serial?
|
22
|
+
port
|
23
|
+
else
|
24
|
+
"#{host}:#{port}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def parse(data)
|
31
|
+
# is TCP host/port?
|
32
|
+
if m = /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d{1,5})/.match(data)
|
33
|
+
@port = m[2]
|
34
|
+
@host = m[1]
|
35
|
+
@is_tcp = true
|
36
|
+
|
37
|
+
# is it a numeric port for localhost tcp?
|
38
|
+
elsif /^[0-9]{1,5}$/.match(data)
|
39
|
+
@port = data
|
40
|
+
@host = "localhost"
|
41
|
+
@is_tcp = true
|
42
|
+
|
43
|
+
# must be a serial port
|
44
|
+
else
|
45
|
+
@port = data
|
46
|
+
@host = nil
|
47
|
+
@is_serial = true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/artoo/robot.rb
ADDED
@@ -0,0 +1,299 @@
|
|
1
|
+
require 'celluloid/io'
|
2
|
+
require 'multi_json'
|
3
|
+
|
4
|
+
require 'artoo/connection'
|
5
|
+
require 'artoo/device'
|
6
|
+
require 'artoo/api'
|
7
|
+
require 'artoo/master'
|
8
|
+
require 'artoo/port'
|
9
|
+
require 'artoo/utility'
|
10
|
+
|
11
|
+
module Artoo
|
12
|
+
# The most important class used by Artoo is Robot. This represents the primary
|
13
|
+
# interface for interacting with a collection of physical computing capabilities.
|
14
|
+
class Robot
|
15
|
+
include Celluloid
|
16
|
+
include Celluloid::Notifications
|
17
|
+
include Artoo::Utility
|
18
|
+
|
19
|
+
attr_reader :connections, :devices, :name
|
20
|
+
|
21
|
+
def initialize(params={})
|
22
|
+
@name = params[:name] || "Robot #{random_string}"
|
23
|
+
initialize_connections(params[:connections] || {})
|
24
|
+
initialize_devices(params[:devices] || {})
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
attr_accessor :connection_types, :device_types, :working_code,
|
29
|
+
:use_api, :api_host, :api_port
|
30
|
+
|
31
|
+
# connection to some hardware that has one or more devices via some specific protocol
|
32
|
+
# Example:
|
33
|
+
# connection :arduino, :adaptor => :firmata, :port => '/dev/tty.usbmodemxxxxx'
|
34
|
+
def connection(name, params = {})
|
35
|
+
Celluloid::Logger.info "Registering connection '#{name}'..."
|
36
|
+
self.connection_types ||= []
|
37
|
+
self.connection_types << {:name => name}.merge(params)
|
38
|
+
end
|
39
|
+
|
40
|
+
# device that uses a connection to communicate
|
41
|
+
# Example:
|
42
|
+
# device :collision_detect, :driver => :switch, :pin => 3
|
43
|
+
def device(name, params = {})
|
44
|
+
Celluloid::Logger.info "Registering device '#{name}'..."
|
45
|
+
self.device_types ||= []
|
46
|
+
self.device_types << {:name => name}.merge(params)
|
47
|
+
end
|
48
|
+
|
49
|
+
# the work that needs to be performed
|
50
|
+
# Example:
|
51
|
+
# work do
|
52
|
+
# every(10.seconds) do
|
53
|
+
# puts "hello, world"
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
def work(&block)
|
57
|
+
Celluloid::Logger.info "Preparing work..."
|
58
|
+
self.working_code = block if block_given?
|
59
|
+
end
|
60
|
+
|
61
|
+
# activate RESTful api
|
62
|
+
# Example:
|
63
|
+
# api :host => '127.0.0.1', :port => '1234'
|
64
|
+
def api(params = {})
|
65
|
+
Celluloid::Logger.info "Registering api..."
|
66
|
+
self.use_api = true
|
67
|
+
self.api_host = params[:host] || '127.0.0.1'
|
68
|
+
self.api_port = params[:port] || '4321'
|
69
|
+
end
|
70
|
+
|
71
|
+
# work can be performed by either:
|
72
|
+
# an existing instance
|
73
|
+
# an array of existing instances
|
74
|
+
# or, a new instance can be created
|
75
|
+
def work!(robot=nil)
|
76
|
+
if robot.respond_to?(:work)
|
77
|
+
robots = [robot]
|
78
|
+
elsif robot.kind_of?(Array)
|
79
|
+
robots = robot
|
80
|
+
else
|
81
|
+
robots = [self.new]
|
82
|
+
end
|
83
|
+
|
84
|
+
robots.each {|r| r.async.work}
|
85
|
+
|
86
|
+
Celluloid::Actor[:master] = Master.new(robots)
|
87
|
+
Celluloid::Actor[:api] = Api.new(self.api_host, self.api_port) if self.use_api
|
88
|
+
|
89
|
+
sleep # sleep main thread, and let the work commence!
|
90
|
+
end
|
91
|
+
|
92
|
+
def test?
|
93
|
+
ENV["ARTOO_TEST"] == 'true'
|
94
|
+
end
|
95
|
+
|
96
|
+
# Taken from Sinatra codebase
|
97
|
+
# Sets an option to the given value. If the value is a proc,
|
98
|
+
# the proc will be called every time the option is accessed.
|
99
|
+
def set(option, value = (not_set = true), ignore_setter = false, &block)
|
100
|
+
raise ArgumentError if block and !not_set
|
101
|
+
value, not_set = block, false if block
|
102
|
+
|
103
|
+
if not_set
|
104
|
+
raise ArgumentError unless option.respond_to?(:each)
|
105
|
+
option.each { |k,v| set(k, v) }
|
106
|
+
return self
|
107
|
+
end
|
108
|
+
|
109
|
+
if respond_to?("#{option}=") and not ignore_setter
|
110
|
+
return __send__("#{option}=", value)
|
111
|
+
end
|
112
|
+
|
113
|
+
setter = proc { |val| set option, val, true }
|
114
|
+
getter = proc { value }
|
115
|
+
|
116
|
+
case value
|
117
|
+
when Proc
|
118
|
+
getter = value
|
119
|
+
when Symbol, Fixnum, FalseClass, TrueClass, NilClass
|
120
|
+
getter = value.inspect
|
121
|
+
when Hash
|
122
|
+
setter = proc do |val|
|
123
|
+
val = value.merge val if Hash === val
|
124
|
+
set option, val, true
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
define_singleton_method("#{option}=", setter) if setter
|
129
|
+
define_singleton_method(option, getter) if getter
|
130
|
+
define_singleton_method("#{option}?", "!!#{option}") unless method_defined? "#{option}?"
|
131
|
+
self
|
132
|
+
end
|
133
|
+
|
134
|
+
# Taken from Sinatra codebase
|
135
|
+
CALLERS_TO_IGNORE = [ # :nodoc:
|
136
|
+
/lib\/artoo.*\.rb$/, # artoo code
|
137
|
+
/^\(.*\)$/, # generated code
|
138
|
+
/rubygems\/custom_require\.rb$/, # rubygems require hacks
|
139
|
+
/active_support/, # active_support require hacks
|
140
|
+
/bundler(\/runtime)?\.rb/, # bundler require hacks
|
141
|
+
/<internal:/, # internal in ruby >= 1.9.2
|
142
|
+
/src\/kernel\/bootstrap\/[A-Z]/ # maglev kernel files
|
143
|
+
]
|
144
|
+
|
145
|
+
# Taken from Sinatra codebase
|
146
|
+
# Like Kernel#caller but excluding certain magic entries and without
|
147
|
+
# line / method information; the resulting array contains filenames only.
|
148
|
+
def caller_files
|
149
|
+
cleaned_caller(1).flatten
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
# Taken from Sinatra codebase
|
155
|
+
def define_singleton_method(name, content = Proc.new)
|
156
|
+
# replace with call to singleton_class once we're 1.9 only
|
157
|
+
(class << self; self; end).class_eval do
|
158
|
+
undef_method(name) if method_defined? name
|
159
|
+
String === content ? class_eval("def #{name}() #{content}; end") : define_method(name, &content)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Taken from Sinatra codebase
|
164
|
+
# Like Kernel#caller but excluding certain magic entries
|
165
|
+
def cleaned_caller(keep = 3)
|
166
|
+
caller(1).
|
167
|
+
map { |line| line.split(/:(?=\d|in )/, 3)[0,keep] }.
|
168
|
+
reject { |file, *_| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def safe_name
|
173
|
+
name.gsub(' ', '_').downcase
|
174
|
+
end
|
175
|
+
|
176
|
+
def api_host
|
177
|
+
self.class.api_host
|
178
|
+
end
|
179
|
+
|
180
|
+
def api_port
|
181
|
+
self.class.api_port
|
182
|
+
end
|
183
|
+
|
184
|
+
# start doing the work
|
185
|
+
def work
|
186
|
+
Logger.info "Starting work..."
|
187
|
+
make_connections
|
188
|
+
start_devices
|
189
|
+
current_instance.instance_eval(&working_code)
|
190
|
+
rescue Exception => e
|
191
|
+
Logger.error e.message
|
192
|
+
Logger.error e.backtrace.inspect
|
193
|
+
end
|
194
|
+
|
195
|
+
def make_connections
|
196
|
+
result = false
|
197
|
+
future_connections = []
|
198
|
+
# block until all connections done
|
199
|
+
connections.each {|k, c| future_connections << c.future.connect}
|
200
|
+
future_connections.each {|v| result = v.value}
|
201
|
+
result
|
202
|
+
end
|
203
|
+
|
204
|
+
def start_devices
|
205
|
+
result = false
|
206
|
+
future_devices = []
|
207
|
+
# block until all devices done
|
208
|
+
devices.each {|k, d| future_devices << d.future.start_device}
|
209
|
+
future_devices.each {|v| result = v.value}
|
210
|
+
result
|
211
|
+
end
|
212
|
+
|
213
|
+
def disconnect
|
214
|
+
connections.each {|k, c| c.async.disconnect}
|
215
|
+
end
|
216
|
+
|
217
|
+
def default_connection
|
218
|
+
connections[connections.keys.first]
|
219
|
+
end
|
220
|
+
|
221
|
+
def connection_types
|
222
|
+
current_class.connection_types ||= []
|
223
|
+
current_class.connection_types
|
224
|
+
end
|
225
|
+
|
226
|
+
def device_types
|
227
|
+
current_class.device_types ||= []
|
228
|
+
current_class.device_types
|
229
|
+
end
|
230
|
+
|
231
|
+
def working_code
|
232
|
+
current_class.working_code ||= proc {puts "No work defined."}
|
233
|
+
end
|
234
|
+
|
235
|
+
# Subscribe to an event from a device
|
236
|
+
def on(device, events={})
|
237
|
+
events.each do |k, v|
|
238
|
+
subscribe("#{safe_name}_#{device.name}_#{k}", create_proxy_method(k, v))
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# Create an anonymous subscription method so we can wrap the
|
243
|
+
# subscription method fire into a valid method regardless
|
244
|
+
# of where it is defined
|
245
|
+
def create_proxy_method(k, v)
|
246
|
+
proxy_method_name(k).tap do |name|
|
247
|
+
self.class.send :define_method, name do |*args|
|
248
|
+
case v
|
249
|
+
when Symbol
|
250
|
+
self.send v.to_sym, *args
|
251
|
+
when Proc
|
252
|
+
v.call(*args)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# A simple loop to create a 'fake' anonymous method
|
259
|
+
def proxy_method_name(k)
|
260
|
+
begin
|
261
|
+
meth = "#{k}_#{Random.rand(999)}"
|
262
|
+
end while respond_to?(meth)
|
263
|
+
meth
|
264
|
+
end
|
265
|
+
|
266
|
+
def to_hash
|
267
|
+
{:name => name,
|
268
|
+
:connections => connections.each_value.collect {|c|c.to_hash},
|
269
|
+
:devices => devices.each_value.collect {|d|d.to_hash}
|
270
|
+
}
|
271
|
+
end
|
272
|
+
|
273
|
+
def as_json
|
274
|
+
MultiJson.dump(to_hash)
|
275
|
+
end
|
276
|
+
|
277
|
+
private
|
278
|
+
|
279
|
+
def initialize_connections(params={})
|
280
|
+
@connections = {}
|
281
|
+
connection_types.each {|ct|
|
282
|
+
Logger.info "Initializing connection #{ct[:name].to_s}..."
|
283
|
+
cp = params[ct[:name]] || {}
|
284
|
+
c = Connection.new(ct.merge(cp).merge(:parent => current_instance))
|
285
|
+
@connections[ct[:name]] = c
|
286
|
+
}
|
287
|
+
end
|
288
|
+
|
289
|
+
def initialize_devices(params={})
|
290
|
+
@devices = {}
|
291
|
+
device_types.each {|d|
|
292
|
+
Logger.info "Initializing device #{d[:name].to_s}..."
|
293
|
+
d = Device.new(d.merge(:parent => current_instance))
|
294
|
+
instance_eval("def #{d.name}; return devices[:#{d.name}]; end")
|
295
|
+
@devices[d.name.intern] = d
|
296
|
+
}
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
module Artoo
|
4
|
+
module Utility
|
5
|
+
def constantize(camel_cased_word)
|
6
|
+
ActiveSupport::Inflector.constantize(camel_cased_word)
|
7
|
+
end
|
8
|
+
|
9
|
+
def classify(underscored)
|
10
|
+
ActiveSupport::Inflector.camelize(underscored.to_s.sub(/.*\./, ''))
|
11
|
+
end
|
12
|
+
|
13
|
+
def random_string
|
14
|
+
(0...8).map{65.+(rand(26)).chr}.join
|
15
|
+
end
|
16
|
+
|
17
|
+
def current_instance
|
18
|
+
Celluloid::Actor.current
|
19
|
+
end
|
20
|
+
|
21
|
+
def current_class
|
22
|
+
Celluloid::Actor.current.class
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# just a bit of syntactic sugar, actually does nothing
|
28
|
+
# Example:
|
29
|
+
# 20.seconds => 20
|
30
|
+
# 1.second => 1
|
31
|
+
class Integer < Numeric
|
32
|
+
def seconds
|
33
|
+
return self
|
34
|
+
end
|
35
|
+
|
36
|
+
def second
|
37
|
+
return self
|
38
|
+
end
|
39
|
+
end
|