artoo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. data/.gitignore +25 -0
  2. data/Gemfile +26 -0
  3. data/Gemfile.lock +144 -0
  4. data/Guardfile +15 -0
  5. data/LICENSE +13 -0
  6. data/README.md +97 -0
  7. data/Rakefile +11 -0
  8. data/api/assets/compass.rb +25 -0
  9. data/api/assets/javascripts/artoo/controllers/robot.js.coffee +27 -0
  10. data/api/assets/javascripts/artoo/routes.js.coffee +23 -0
  11. data/api/assets/javascripts/core.js.coffee +6 -0
  12. data/api/assets/javascripts/vendor/angular.min.js +161 -0
  13. data/api/assets/javascripts/vendor/bootstrap.min.js +6 -0
  14. data/api/assets/javascripts/vendor/jquery.min.js +5 -0
  15. data/api/assets/stylesheets/artoo/_core.css.scss +3 -0
  16. data/api/assets/stylesheets/artoo/_font-awesome.scss +534 -0
  17. data/api/assets/stylesheets/artoo/_variables.scss +8 -0
  18. data/api/assets/stylesheets/core.scss +70 -0
  19. data/api/public/core.css +9848 -0
  20. data/api/public/core.js +259 -0
  21. data/api/public/favicon.ico +0 -0
  22. data/api/public/font/FontAwesome.otf +0 -0
  23. data/api/public/font/fontawesome-webfont.eot +0 -0
  24. data/api/public/font/fontawesome-webfont.svg +284 -0
  25. data/api/public/font/fontawesome-webfont.ttf +0 -0
  26. data/api/public/font/fontawesome-webfont.woff +0 -0
  27. data/api/public/html5shiv.js +8 -0
  28. data/api/public/images/devices/ardrone.jpg +0 -0
  29. data/api/public/images/devices/arduino.jpg +0 -0
  30. data/api/public/images/devices/sphero.png +0 -0
  31. data/api/public/images/glyphicons-halflings-white.png +0 -0
  32. data/api/public/images/glyphicons-halflings.png +0 -0
  33. data/api/public/index.html +36 -0
  34. data/api/public/partials/robot-detail.html +111 -0
  35. data/api/public/partials/robot-device-detail.html +0 -0
  36. data/api/public/partials/robot-index.html +26 -0
  37. data/artoo.gemspec +21 -0
  38. data/bin/retry.sh +8 -0
  39. data/bin/sphero.sh +8 -0
  40. data/examples/ardrone.rb +12 -0
  41. data/examples/ardrone_nav.rb +22 -0
  42. data/examples/ardrone_nav_video.rb +33 -0
  43. data/examples/ardrone_video.rb +22 -0
  44. data/examples/conway_sphero.rb +65 -0
  45. data/examples/firmata.rb +13 -0
  46. data/examples/firmata_button.rb +9 -0
  47. data/examples/hello.rb +12 -0
  48. data/examples/hello_api.rb +9 -0
  49. data/examples/hello_api_multiple.rb +25 -0
  50. data/examples/hello_modular.rb +16 -0
  51. data/examples/hello_multiple.rb +22 -0
  52. data/examples/notifications.rb +9 -0
  53. data/examples/sphero.rb +11 -0
  54. data/examples/sphero_color.rb +13 -0
  55. data/examples/sphero_firmata.rb +17 -0
  56. data/examples/sphero_messages.rb +22 -0
  57. data/examples/sphero_multiple.rb +33 -0
  58. data/examples/wiiclassic.rb +94 -0
  59. data/lib/artoo.rb +3 -0
  60. data/lib/artoo/adaptors/adaptor.rb +54 -0
  61. data/lib/artoo/adaptors/ardrone.rb +32 -0
  62. data/lib/artoo/adaptors/ardrone_navigation.rb +26 -0
  63. data/lib/artoo/adaptors/ardrone_video.rb +27 -0
  64. data/lib/artoo/adaptors/firmata.rb +25 -0
  65. data/lib/artoo/adaptors/loopback.rb +8 -0
  66. data/lib/artoo/adaptors/sphero.rb +46 -0
  67. data/lib/artoo/api.rb +48 -0
  68. data/lib/artoo/api_route_helpers.rb +197 -0
  69. data/lib/artoo/connection.rb +70 -0
  70. data/lib/artoo/delegator.rb +49 -0
  71. data/lib/artoo/device.rb +61 -0
  72. data/lib/artoo/device_event_client.rb +27 -0
  73. data/lib/artoo/drivers/ardrone.rb +9 -0
  74. data/lib/artoo/drivers/ardrone_navigation.rb +21 -0
  75. data/lib/artoo/drivers/ardrone_video.rb +22 -0
  76. data/lib/artoo/drivers/button.rb +40 -0
  77. data/lib/artoo/drivers/driver.rb +48 -0
  78. data/lib/artoo/drivers/led.rb +37 -0
  79. data/lib/artoo/drivers/passthru.rb +9 -0
  80. data/lib/artoo/drivers/pinger.rb +19 -0
  81. data/lib/artoo/drivers/pinger2.rb +18 -0
  82. data/lib/artoo/drivers/sphero.rb +57 -0
  83. data/lib/artoo/drivers/wiichuck.rb +29 -0
  84. data/lib/artoo/drivers/wiiclassic.rb +137 -0
  85. data/lib/artoo/main.rb +32 -0
  86. data/lib/artoo/master.rb +16 -0
  87. data/lib/artoo/port.rb +51 -0
  88. data/lib/artoo/robot.rb +299 -0
  89. data/lib/artoo/utility.rb +39 -0
  90. data/lib/artoo/version.rb +5 -0
  91. data/test/adaptors/adaptor_test.rb +18 -0
  92. data/test/adaptors/ardrone_test.rb +24 -0
  93. data/test/adaptors/firmata_test.rb +25 -0
  94. data/test/adaptors/loopback_test.rb +18 -0
  95. data/test/adaptors/sphero_test.rb +24 -0
  96. data/test/api_test.rb +61 -0
  97. data/test/artoo_test.rb +12 -0
  98. data/test/connection_test.rb +28 -0
  99. data/test/delegator_test.rb +71 -0
  100. data/test/device_test.rb +41 -0
  101. data/test/drivers/ardrone_navigation_test.rb +11 -0
  102. data/test/drivers/ardrone_test.rb +11 -0
  103. data/test/drivers/ardrone_video_test.rb +11 -0
  104. data/test/drivers/driver_test.rb +21 -0
  105. data/test/drivers/led_test.rb +52 -0
  106. data/test/drivers/sphero_test.rb +54 -0
  107. data/test/drivers/wiichuck_test.rb +11 -0
  108. data/test/port_test.rb +33 -0
  109. data/test/robot_test.rb +96 -0
  110. data/test/test_helper.rb +8 -0
  111. data/test/utility_test.rb +27 -0
  112. metadata +185 -0
@@ -0,0 +1,8 @@
1
+ require 'artoo/adaptors/adaptor'
2
+
3
+ module Artoo
4
+ module Adaptors
5
+ class Loopback < Adaptor
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,46 @@
1
+ require 'artoo/adaptors/adaptor'
2
+
3
+ module Artoo
4
+ module Adaptors
5
+ # Connect to a Sphero (http://gosphero.com)
6
+ class Sphero < Adaptor
7
+ RETRY_COUNT = 5
8
+ attr_reader :sphero
9
+
10
+ def finalize
11
+ if connected?
12
+ #sphero.stop
13
+ sphero.close
14
+ end
15
+ end
16
+
17
+ def connect
18
+ @retries_left = RETRY_COUNT
19
+ require 'sphero' unless defined?(::Sphero)
20
+ begin
21
+ @sphero = ::Sphero.new(connect_to)
22
+ super
23
+ return true
24
+ rescue Errno::EBUSY => e
25
+ @retries_left -= 1
26
+ if @retries_left > 0
27
+ retry
28
+ else
29
+ Logger.error e.message
30
+ Logger.error e.backtrace.inspect
31
+ return false
32
+ end
33
+ end
34
+ end
35
+
36
+ def disconnect
37
+ sphero.close
38
+ super
39
+ end
40
+
41
+ def method_missing(method_name, *arguments, &block)
42
+ sphero.send(method_name, *arguments, &block)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,48 @@
1
+ require 'reel'
2
+ require 'artoo/api_route_helpers'
3
+ require 'artoo/device_event_client'
4
+
5
+ module Artoo
6
+ class Api < Reel::Server
7
+ include Artoo::ApiRouteHelpers
8
+
9
+ def initialize(host = "127.0.0.1", port = 3000)
10
+ super(host, port, &method(:on_connection))
11
+ end
12
+
13
+ def on_connection(connection)
14
+ while request = connection.request
15
+ dispatch!(connection, request)
16
+ end
17
+ end
18
+
19
+ get '/robots' do
20
+ MultiJson.dump(Actor[:master].robots.collect {|r|r.to_hash})
21
+ end
22
+
23
+ get '/robots/:robotid' do
24
+ Actor[:master].get_robot_by_name(@params['robotid']).as_json
25
+ end
26
+
27
+ get '/robots/:robotid/devices' do
28
+ MultiJson.dump(Actor[:master].get_robot_by_name(@params['robotid']).devices.each_value.collect {|d| d.to_hash})
29
+ end
30
+
31
+ get '/robots/:robotid/devices/:deviceid' do
32
+ Actor[:master].get_robot_by_name(@params['robotid']).devices[@params['deviceid'].intern].as_json
33
+ end
34
+
35
+ get_ws '/robots/:robotid/devices/:deviceid/events' do
36
+ DeviceEventClient.new(@req, Actor[:master].get_robot_by_name(@params['robotid']).devices[@params['deviceid'].intern].event_topic_name('update'))
37
+ return nil
38
+ end
39
+
40
+ get '/robots/:robotid/connections' do
41
+ MultiJson.dump(Actor[:master].get_robot_by_name(@params['robotid']).connections.each_value.collect {|c| c.to_hash})
42
+ end
43
+
44
+ get '/robots/:robotid/connections/:connectionid' do
45
+ Actor[:master].get_robot_by_name(@params['robotid']).connections[@params['connectionid'].intern].as_json
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,197 @@
1
+ # route helpers used within the Artoo::Api class
2
+ module Artoo
3
+ module ApiRouteHelpers
4
+ class ResponseHandled < StandardError; end
5
+ module ClassMethods
6
+
7
+ def static_path(default=File.join(File.dirname(__FILE__), "..", "..", "api/public"))
8
+ @static_path ||= default
9
+ end
10
+
11
+ def routes
12
+ @routes ||= {}
13
+ end
14
+
15
+ def route(verb, path, &block)
16
+ signature = compile!(verb, path, block, {})
17
+ (routes[verb] ||= []) << signature
18
+ end
19
+
20
+ ## Ripped from Sinatra 'cause it's so sexy in there
21
+ def generate_method(method_name, &block)
22
+ define_method(method_name, &block)
23
+ method = instance_method method_name
24
+ remove_method method_name
25
+ method
26
+ end
27
+
28
+ def compile!(verb, path, block, options = {})
29
+ options.each_pair { |option, args| send(option, *args) }
30
+ method_name = "#{verb} #{path}"
31
+ unbound_method = generate_method(method_name, &block)
32
+ pattern, keys = compile path
33
+ conditions, @conditions = @conditions, []
34
+
35
+ [ pattern, keys, conditions, block.arity != 0 ?
36
+ proc { |a,p| unbound_method.bind(a).call(*p) } :
37
+ proc { |a,p| unbound_method.bind(a).call } ]
38
+ end
39
+
40
+ def compile(path)
41
+ keys = []
42
+ if path.respond_to? :to_str
43
+ ignore = ""
44
+ pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) do |c|
45
+ ignore << escaped(c).join if c.match(/[\.@]/)
46
+ patt = encoded(c)
47
+ patt.gsub(/%[\da-fA-F]{2}/) do |match|
48
+ match.split(//).map {|char| char =~ /[A-Z]/ ? "[#{char}#{char.tr('A-Z', 'a-z')}]" : char}.join
49
+ end
50
+ end
51
+ pattern.gsub!(/((:\w+)|\*)/) do |match|
52
+ if match == "*"
53
+ keys << 'splat'
54
+ "(.*?)"
55
+ else
56
+ keys << $2[1..-1]
57
+ ignore_pattern = safe_ignore(ignore)
58
+
59
+ ignore_pattern
60
+ end
61
+ end
62
+ [/\A#{pattern}\z/, keys]
63
+ elsif path.respond_to?(:keys) && path.respond_to?(:match)
64
+ [path, path.keys]
65
+ elsif path.respond_to?(:names) && path.respond_to?(:match)
66
+ [path, path.names]
67
+ elsif path.respond_to? :match
68
+ [path, keys]
69
+ else
70
+ raise TypeError, path
71
+ end
72
+ end
73
+
74
+ def safe_ignore(ignore)
75
+ unsafe_ignore = []
76
+ ignore = ignore.gsub(/%[\da-fA-F]{2}/) do |hex|
77
+ unsafe_ignore << hex[1..2]
78
+ ''
79
+ end
80
+ unsafe_patterns = unsafe_ignore.map do |unsafe|
81
+ chars = unsafe.split(//).map do |char|
82
+ if char =~ /[A-Z]/
83
+ char <<= char.tr('A-Z', 'a-z')
84
+ end
85
+ char
86
+ end
87
+
88
+ "|(?:%[^#{chars[0]}].|%[#{chars[0]}][^#{chars[1]}])"
89
+ end
90
+ if unsafe_patterns.length > 0
91
+ "((?:[^#{ignore}/?#%]#{unsafe_patterns.join()})+)"
92
+ else
93
+ "([^#{ignore}/?#]+)"
94
+ end
95
+ end
96
+
97
+ # Helper route functions
98
+ def get(path, &block)
99
+ route 'GET', path, &block
100
+ end
101
+ def get_ws(path, &block)
102
+ route 'GET', path, &block
103
+ end
104
+ def put(path, &block)
105
+ route 'PUT', path, &block
106
+ end
107
+ end
108
+
109
+ module InstanceMethods
110
+ ## Handle the request
111
+ def dispatch!(connection, req)
112
+ resp = catch(:halt) do
113
+ try_static! connection, req
114
+ route! connection, req
115
+ end
116
+ if resp && !resp.nil?
117
+ return if req.is_a?(Reel::WebSocket)
118
+ status, body = resp
119
+ req.respond status, body
120
+ else
121
+ req.respond :not_found, "NOT FOUND"
122
+ end
123
+ end
124
+
125
+ # Exit the current block, halts any further processing
126
+ # of the request, and returns the specified response.
127
+ def halt(*response)
128
+ response = response.first if response.length == 1
129
+ throw :halt, response
130
+ end
131
+
132
+ def try_static!(connection, req)
133
+ fpath = req.url == '/' ? 'index.html' : req.url[1..-1]
134
+ filepath = File.expand_path(fpath, self.class.static_path)
135
+ if File.file?(filepath)
136
+ # TODO: stream this?
137
+ data = open(filepath).read
138
+ halt :ok, data
139
+ end
140
+ end
141
+
142
+ def route!(connection, req)
143
+ if routes = self.class.routes[req.method]
144
+ routes.each do |pattern, keys, conditions, block|
145
+ route = req.url
146
+ next unless match = pattern.match(route)
147
+ values = match.captures.to_a.map { |v| URI.decode_www_form_component(v) if v }
148
+ if values.any?
149
+ params = {}
150
+ keys.zip(values) { |k,v| Array === params[k] ? params[k] << v : params[k] = v if v }
151
+ @params = params
152
+ end
153
+
154
+ @connection = connection
155
+ @req = req
156
+
157
+ begin
158
+ body = block ? block[self, values] : yield(self, values)
159
+ halt [:ok, body]
160
+ rescue Exception => e
161
+ p [:e, e]
162
+ end
163
+ end
164
+ end
165
+ nil
166
+ end
167
+
168
+ # Fixes encoding issues by
169
+ # * defaulting to UTF-8
170
+ # * casting params to Encoding.default_external
171
+ #
172
+ # The latter might not be necessary if Rack handles it one day.
173
+ # Keep an eye on Rack's LH #100.
174
+ def force_encoding(*args) settings.force_encoding(*args) end
175
+ if defined? Encoding
176
+ def self.force_encoding(data, encoding = default_encoding)
177
+ return if data == settings || data.is_a?(Tempfile)
178
+ if data.respond_to? :force_encoding
179
+ data.force_encoding(encoding).encode!
180
+ elsif data.respond_to? :each_value
181
+ data.each_value { |v| force_encoding(v, encoding) }
182
+ elsif data.respond_to? :each
183
+ data.each { |v| force_encoding(v, encoding) }
184
+ end
185
+ data
186
+ end
187
+ else
188
+ def self.force_encoding(data, *) data end
189
+ end
190
+ end
191
+
192
+ def self.included(receiver)
193
+ receiver.extend ClassMethods
194
+ receiver.send :include, InstanceMethods
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,70 @@
1
+ require 'artoo/utility'
2
+
3
+ module Artoo
4
+ # The Connection class represents the interface to
5
+ # a specific group of hardware devices. Examples would be an
6
+ # Arduino, a Sphero, or an ARDrone.
7
+ class Connection
8
+ include Celluloid
9
+ include Artoo::Utility
10
+
11
+ attr_reader :parent, :name, :port, :adaptor
12
+
13
+ def initialize(params={})
14
+ @name = params[:name].to_s
15
+ @port = Port.new(params[:port])
16
+ @parent = params[:parent]
17
+
18
+ require_adaptor(params[:adaptor] || :loopback)
19
+ end
20
+
21
+ def connect
22
+ Logger.info "Connecting to '#{name}' on port '#{port}'..."
23
+ adaptor.connect
24
+ rescue Exception => e
25
+ Logger.error e.message
26
+ Logger.error e.backtrace.inspect
27
+ end
28
+
29
+ def disconnect
30
+ Logger.info "Disconnecting from '#{name}' on port '#{port}'..."
31
+ adaptor.disconnect
32
+ end
33
+
34
+ def connected?
35
+ adaptor.connected?
36
+ end
37
+
38
+ def to_hash
39
+ {:name => name,
40
+ :port => port.to_s,
41
+ :adaptor => adaptor.class.name.demodulize,
42
+ :connected => connected?
43
+ }
44
+ end
45
+
46
+ def as_json
47
+ MultiJson.dump(to_hash)
48
+ end
49
+
50
+ def method_missing(method_name, *arguments, &block)
51
+ unless adaptor.connected?
52
+ Logger.warn "Cannot call unconnected adaptor '#{name}', attempting to reconnect..."
53
+ adaptor.reconnect
54
+ return nil
55
+ end
56
+ adaptor.send(method_name, *arguments, &block)
57
+ rescue Exception => e
58
+ Logger.error e.message
59
+ Logger.error e.backtrace.inspect
60
+ return nil
61
+ end
62
+
63
+ private
64
+
65
+ def require_adaptor(type)
66
+ require "artoo/adaptors/#{type.to_s}"
67
+ @adaptor = constantize("Artoo::Adaptors::#{classify(type.to_s)}").new(:port => port, :parent => current_instance)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,49 @@
1
+
2
+ module Artoo
3
+ # Execution context for top-level robots
4
+ # DSL methods executed on main are delegated to this class like Sinatra
5
+ class MainRobot < Robot
6
+ #set :logging, Proc.new { ! test? }
7
+ #set :method_override, true
8
+ set :start_work, false #Proc.new { ! test? }
9
+ #set :app_file, nil
10
+
11
+ # def self.register(*extensions, &block) #:nodoc:
12
+ # added_methods = extensions.map {|m| m.public_instance_methods }.flatten
13
+ # Delegator.delegate(*added_methods)
14
+ # super(*extensions, &block)
15
+ # end
16
+ end
17
+
18
+ # Artoo delegation mixin that acts like Sinatra.
19
+ # Mixing this module into an object causes all
20
+ # methods to be delegated to the Artoo::MainRobot class.
21
+ # Used primarily at the top-level.
22
+ module Delegator #:nodoc:
23
+ def self.delegate(*methods)
24
+ methods.each do |method_name|
25
+ define_method(method_name) do |*args, &block|
26
+ return super(*args, &block) if respond_to? method_name
27
+ Delegator.target.send(method_name, *args, &block)
28
+ end
29
+ private method_name
30
+ end
31
+ end
32
+
33
+ delegate :connection, :device, :work, :api, :set, :test?
34
+
35
+ class << self
36
+ attr_accessor :target
37
+ end
38
+
39
+ self.target = Artoo::MainRobot
40
+ end
41
+
42
+ # Create a new Artoo robot. The block is evaluated
43
+ # in the new robot's class scope.
44
+ def self.new(robot=Robot, options={}, &block)
45
+ robot = Class.new(robot)
46
+ robot.class_eval(&block) if block_given?
47
+ robot
48
+ end
49
+ end
@@ -0,0 +1,61 @@
1
+ module Artoo
2
+ # The Artoo::Device class represents the interface to
3
+ # a specific individual hardware devices. Examples would be a digital
4
+ # thermometer connected to an Arduino, or a Sphero's accelerometer.
5
+ class Device
6
+ include Celluloid
7
+ include Artoo::Utility
8
+
9
+ attr_reader :parent, :name, :driver, :pin, :connection, :interval
10
+
11
+ def initialize(params={})
12
+ @name = params[:name].to_s
13
+ @pin = params[:pin]
14
+ @parent = params[:parent]
15
+ @connection = determine_connection(params[:connection]) || default_connection
16
+ @interval = params[:interval] || 0.5
17
+
18
+ require_driver(params[:driver] || :passthru)
19
+ end
20
+
21
+ def determine_connection(c)
22
+ parent.connections[c] unless c.nil?
23
+ end
24
+
25
+ def default_connection
26
+ parent.default_connection
27
+ end
28
+
29
+ def start_device
30
+ driver.start_driver
31
+ end
32
+
33
+ def event_topic_name(event)
34
+ "#{parent.safe_name}_#{name}_#{event}"
35
+ end
36
+
37
+ def to_hash
38
+ {:name => name,
39
+ :driver => driver.class.name.demodulize,
40
+ :pin => pin.to_s,
41
+ :connection => connection.to_hash,
42
+ :interval => interval
43
+ }
44
+ end
45
+
46
+ def as_json
47
+ MultiJson.dump(to_hash)
48
+ end
49
+
50
+ def method_missing(method_name, *arguments, &block)
51
+ driver.send(method_name, *arguments, &block)
52
+ end
53
+
54
+ private
55
+
56
+ def require_driver(d)
57
+ require "artoo/drivers/#{d.to_s}"
58
+ @driver = constantize("Artoo::Drivers::#{classify(d.to_s)}").new(:parent => current_instance)
59
+ end
60
+ end
61
+ end