artoo 0.1.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 (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