roby 0.7

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 (240) hide show
  1. data/.gitignore +29 -0
  2. data/History.txt +4 -0
  3. data/License-fr.txt +519 -0
  4. data/License.txt +515 -0
  5. data/Manifest.txt +245 -0
  6. data/NOTES +4 -0
  7. data/README.txt +163 -0
  8. data/Rakefile +161 -0
  9. data/TODO.txt +146 -0
  10. data/app/README.txt +24 -0
  11. data/app/Rakefile +8 -0
  12. data/app/config/ROBOT.rb +5 -0
  13. data/app/config/app.yml +91 -0
  14. data/app/config/init.rb +7 -0
  15. data/app/config/roby.yml +3 -0
  16. data/app/controllers/.gitattributes +0 -0
  17. data/app/controllers/ROBOT.rb +2 -0
  18. data/app/data/.gitattributes +0 -0
  19. data/app/planners/ROBOT/main.rb +6 -0
  20. data/app/planners/main.rb +5 -0
  21. data/app/scripts/distributed +3 -0
  22. data/app/scripts/generate/bookmarks +3 -0
  23. data/app/scripts/replay +3 -0
  24. data/app/scripts/results +3 -0
  25. data/app/scripts/run +3 -0
  26. data/app/scripts/server +3 -0
  27. data/app/scripts/shell +3 -0
  28. data/app/scripts/test +3 -0
  29. data/app/tasks/.gitattributes +0 -0
  30. data/app/tasks/ROBOT/.gitattributes +0 -0
  31. data/bin/roby +210 -0
  32. data/bin/roby-log +168 -0
  33. data/bin/roby-shell +25 -0
  34. data/doc/images/event_generalization.png +0 -0
  35. data/doc/images/exception_propagation_1.png +0 -0
  36. data/doc/images/exception_propagation_2.png +0 -0
  37. data/doc/images/exception_propagation_3.png +0 -0
  38. data/doc/images/exception_propagation_4.png +0 -0
  39. data/doc/images/exception_propagation_5.png +0 -0
  40. data/doc/images/replay_handler_error.png +0 -0
  41. data/doc/images/replay_handler_error_0.png +0 -0
  42. data/doc/images/replay_handler_error_1.png +0 -0
  43. data/doc/images/roby_cycle_overview.png +0 -0
  44. data/doc/images/roby_replay_02.png +0 -0
  45. data/doc/images/roby_replay_03.png +0 -0
  46. data/doc/images/roby_replay_04.png +0 -0
  47. data/doc/images/roby_replay_event_representation.png +0 -0
  48. data/doc/images/roby_replay_first_state.png +0 -0
  49. data/doc/images/roby_replay_relations.png +0 -0
  50. data/doc/images/roby_replay_startup.png +0 -0
  51. data/doc/images/task_event_generalization.png +0 -0
  52. data/doc/papers.rdoc +11 -0
  53. data/doc/styles/allison.css +314 -0
  54. data/doc/styles/allison.js +316 -0
  55. data/doc/styles/allison.rb +276 -0
  56. data/doc/styles/jamis.rb +593 -0
  57. data/doc/tutorials/01-GettingStarted.rdoc +86 -0
  58. data/doc/tutorials/02-GoForward.rdoc +220 -0
  59. data/doc/tutorials/03-PlannedPath.rdoc +268 -0
  60. data/doc/tutorials/04-EventPropagation.rdoc +236 -0
  61. data/doc/tutorials/05-ErrorHandling.rdoc +319 -0
  62. data/doc/tutorials/06-Overview.rdoc +40 -0
  63. data/doc/videos.rdoc +69 -0
  64. data/ext/droby/dump.cc +175 -0
  65. data/ext/droby/extconf.rb +3 -0
  66. data/ext/graph/algorithm.cc +746 -0
  67. data/ext/graph/extconf.rb +7 -0
  68. data/ext/graph/graph.cc +529 -0
  69. data/ext/graph/graph.hh +183 -0
  70. data/ext/graph/iterator_sequence.hh +102 -0
  71. data/ext/graph/undirected_dfs.hh +226 -0
  72. data/ext/graph/undirected_graph.hh +421 -0
  73. data/lib/roby.rb +41 -0
  74. data/lib/roby/app.rb +870 -0
  75. data/lib/roby/app/rake.rb +56 -0
  76. data/lib/roby/app/run.rb +14 -0
  77. data/lib/roby/app/scripts/distributed.rb +13 -0
  78. data/lib/roby/app/scripts/generate/bookmarks.rb +162 -0
  79. data/lib/roby/app/scripts/replay.rb +31 -0
  80. data/lib/roby/app/scripts/results.rb +15 -0
  81. data/lib/roby/app/scripts/run.rb +26 -0
  82. data/lib/roby/app/scripts/server.rb +18 -0
  83. data/lib/roby/app/scripts/shell.rb +88 -0
  84. data/lib/roby/app/scripts/test.rb +40 -0
  85. data/lib/roby/basic_object.rb +151 -0
  86. data/lib/roby/config.rb +5 -0
  87. data/lib/roby/control.rb +747 -0
  88. data/lib/roby/decision_control.rb +17 -0
  89. data/lib/roby/distributed.rb +32 -0
  90. data/lib/roby/distributed/base.rb +440 -0
  91. data/lib/roby/distributed/communication.rb +871 -0
  92. data/lib/roby/distributed/connection_space.rb +592 -0
  93. data/lib/roby/distributed/distributed_object.rb +206 -0
  94. data/lib/roby/distributed/drb.rb +62 -0
  95. data/lib/roby/distributed/notifications.rb +539 -0
  96. data/lib/roby/distributed/peer.rb +550 -0
  97. data/lib/roby/distributed/protocol.rb +529 -0
  98. data/lib/roby/distributed/proxy.rb +343 -0
  99. data/lib/roby/distributed/subscription.rb +311 -0
  100. data/lib/roby/distributed/transaction.rb +498 -0
  101. data/lib/roby/event.rb +897 -0
  102. data/lib/roby/exceptions.rb +234 -0
  103. data/lib/roby/executives/simple.rb +30 -0
  104. data/lib/roby/graph.rb +166 -0
  105. data/lib/roby/interface.rb +390 -0
  106. data/lib/roby/log.rb +3 -0
  107. data/lib/roby/log/chronicle.rb +303 -0
  108. data/lib/roby/log/console.rb +72 -0
  109. data/lib/roby/log/data_stream.rb +197 -0
  110. data/lib/roby/log/dot.rb +279 -0
  111. data/lib/roby/log/event_stream.rb +151 -0
  112. data/lib/roby/log/file.rb +340 -0
  113. data/lib/roby/log/gui/basic_display.ui +83 -0
  114. data/lib/roby/log/gui/chronicle.rb +26 -0
  115. data/lib/roby/log/gui/chronicle_view.rb +40 -0
  116. data/lib/roby/log/gui/chronicle_view.ui +70 -0
  117. data/lib/roby/log/gui/data_displays.rb +172 -0
  118. data/lib/roby/log/gui/data_displays.ui +155 -0
  119. data/lib/roby/log/gui/notifications.rb +26 -0
  120. data/lib/roby/log/gui/relations.rb +248 -0
  121. data/lib/roby/log/gui/relations.ui +123 -0
  122. data/lib/roby/log/gui/relations_view.rb +185 -0
  123. data/lib/roby/log/gui/relations_view.ui +149 -0
  124. data/lib/roby/log/gui/replay.rb +327 -0
  125. data/lib/roby/log/gui/replay_controls.rb +200 -0
  126. data/lib/roby/log/gui/replay_controls.ui +259 -0
  127. data/lib/roby/log/gui/runtime.rb +130 -0
  128. data/lib/roby/log/hooks.rb +185 -0
  129. data/lib/roby/log/logger.rb +202 -0
  130. data/lib/roby/log/notifications.rb +244 -0
  131. data/lib/roby/log/plan_rebuilder.rb +470 -0
  132. data/lib/roby/log/relations.rb +1056 -0
  133. data/lib/roby/log/server.rb +550 -0
  134. data/lib/roby/log/sqlite.rb +47 -0
  135. data/lib/roby/log/timings.rb +164 -0
  136. data/lib/roby/plan-object.rb +247 -0
  137. data/lib/roby/plan.rb +762 -0
  138. data/lib/roby/planning.rb +13 -0
  139. data/lib/roby/planning/loops.rb +302 -0
  140. data/lib/roby/planning/model.rb +906 -0
  141. data/lib/roby/planning/task.rb +151 -0
  142. data/lib/roby/propagation.rb +562 -0
  143. data/lib/roby/query.rb +619 -0
  144. data/lib/roby/relations.rb +583 -0
  145. data/lib/roby/relations/conflicts.rb +70 -0
  146. data/lib/roby/relations/ensured.rb +20 -0
  147. data/lib/roby/relations/error_handling.rb +23 -0
  148. data/lib/roby/relations/events.rb +9 -0
  149. data/lib/roby/relations/executed_by.rb +193 -0
  150. data/lib/roby/relations/hierarchy.rb +239 -0
  151. data/lib/roby/relations/influence.rb +10 -0
  152. data/lib/roby/relations/planned_by.rb +63 -0
  153. data/lib/roby/robot.rb +7 -0
  154. data/lib/roby/standard_errors.rb +218 -0
  155. data/lib/roby/state.rb +5 -0
  156. data/lib/roby/state/events.rb +221 -0
  157. data/lib/roby/state/information.rb +55 -0
  158. data/lib/roby/state/pos.rb +110 -0
  159. data/lib/roby/state/shapes.rb +32 -0
  160. data/lib/roby/state/state.rb +353 -0
  161. data/lib/roby/support.rb +92 -0
  162. data/lib/roby/task-operations.rb +182 -0
  163. data/lib/roby/task.rb +1618 -0
  164. data/lib/roby/test/common.rb +399 -0
  165. data/lib/roby/test/distributed.rb +214 -0
  166. data/lib/roby/test/tasks/empty_task.rb +9 -0
  167. data/lib/roby/test/tasks/goto.rb +36 -0
  168. data/lib/roby/test/tasks/simple_task.rb +23 -0
  169. data/lib/roby/test/testcase.rb +519 -0
  170. data/lib/roby/test/tools.rb +160 -0
  171. data/lib/roby/thread_task.rb +87 -0
  172. data/lib/roby/transactions.rb +462 -0
  173. data/lib/roby/transactions/proxy.rb +292 -0
  174. data/lib/roby/transactions/updates.rb +139 -0
  175. data/plugins/fault_injection/History.txt +4 -0
  176. data/plugins/fault_injection/README.txt +37 -0
  177. data/plugins/fault_injection/Rakefile +18 -0
  178. data/plugins/fault_injection/TODO.txt +0 -0
  179. data/plugins/fault_injection/app.rb +52 -0
  180. data/plugins/fault_injection/fault_injection.rb +89 -0
  181. data/plugins/fault_injection/test/test_fault_injection.rb +84 -0
  182. data/plugins/subsystems/README.txt +40 -0
  183. data/plugins/subsystems/Rakefile +18 -0
  184. data/plugins/subsystems/app.rb +171 -0
  185. data/plugins/subsystems/test/app/README +24 -0
  186. data/plugins/subsystems/test/app/Rakefile +8 -0
  187. data/plugins/subsystems/test/app/config/app.yml +71 -0
  188. data/plugins/subsystems/test/app/config/init.rb +9 -0
  189. data/plugins/subsystems/test/app/config/roby.yml +3 -0
  190. data/plugins/subsystems/test/app/planners/main.rb +20 -0
  191. data/plugins/subsystems/test/app/scripts/distributed +3 -0
  192. data/plugins/subsystems/test/app/scripts/replay +3 -0
  193. data/plugins/subsystems/test/app/scripts/results +3 -0
  194. data/plugins/subsystems/test/app/scripts/run +3 -0
  195. data/plugins/subsystems/test/app/scripts/server +3 -0
  196. data/plugins/subsystems/test/app/scripts/shell +3 -0
  197. data/plugins/subsystems/test/app/scripts/test +3 -0
  198. data/plugins/subsystems/test/app/tasks/services.rb +15 -0
  199. data/plugins/subsystems/test/test_subsystems.rb +71 -0
  200. data/test/distributed/test_communication.rb +178 -0
  201. data/test/distributed/test_connection.rb +282 -0
  202. data/test/distributed/test_execution.rb +373 -0
  203. data/test/distributed/test_mixed_plan.rb +341 -0
  204. data/test/distributed/test_plan_notifications.rb +238 -0
  205. data/test/distributed/test_protocol.rb +516 -0
  206. data/test/distributed/test_query.rb +102 -0
  207. data/test/distributed/test_remote_plan.rb +491 -0
  208. data/test/distributed/test_transaction.rb +463 -0
  209. data/test/mockups/tasks.rb +27 -0
  210. data/test/planning/test_loops.rb +380 -0
  211. data/test/planning/test_model.rb +427 -0
  212. data/test/planning/test_task.rb +106 -0
  213. data/test/relations/test_conflicts.rb +42 -0
  214. data/test/relations/test_ensured.rb +38 -0
  215. data/test/relations/test_executed_by.rb +149 -0
  216. data/test/relations/test_hierarchy.rb +158 -0
  217. data/test/relations/test_planned_by.rb +54 -0
  218. data/test/suite_core.rb +24 -0
  219. data/test/suite_distributed.rb +9 -0
  220. data/test/suite_planning.rb +3 -0
  221. data/test/suite_relations.rb +8 -0
  222. data/test/test_bgl.rb +508 -0
  223. data/test/test_control.rb +399 -0
  224. data/test/test_event.rb +894 -0
  225. data/test/test_exceptions.rb +592 -0
  226. data/test/test_interface.rb +37 -0
  227. data/test/test_log.rb +114 -0
  228. data/test/test_log_server.rb +132 -0
  229. data/test/test_plan.rb +584 -0
  230. data/test/test_propagation.rb +210 -0
  231. data/test/test_query.rb +266 -0
  232. data/test/test_relations.rb +180 -0
  233. data/test/test_state.rb +414 -0
  234. data/test/test_support.rb +16 -0
  235. data/test/test_task.rb +938 -0
  236. data/test/test_testcase.rb +122 -0
  237. data/test/test_thread_task.rb +73 -0
  238. data/test/test_transactions.rb +569 -0
  239. data/test/test_transactions_proxy.rb +198 -0
  240. metadata +570 -0
data/lib/roby/app.rb ADDED
@@ -0,0 +1,870 @@
1
+ require 'roby'
2
+ require 'roby/distributed'
3
+ require 'roby/planning'
4
+ require 'roby/log'
5
+ require 'roby/log/event_stream'
6
+
7
+ require 'roby/robot'
8
+ require 'yaml'
9
+
10
+ module Roby
11
+ # Returns the only one Application object
12
+ def self.app; Application.instance end
13
+
14
+ # = Roby Applications
15
+ #
16
+ # There is one and only one Application object, which holds mainly the
17
+ # system-wide configuration and takes care of file loading and system-wide
18
+ # setup (#setup). A Roby application can be started in multiple modes. The
19
+ # first and most important mode is the runtime mode
20
+ # (<tt>scripts/run</tt>). Other modes are the testing mode (#testing?
21
+ # returns true, entered through <tt>scripts/test</tt>) and the shell mode
22
+ # (#shell? returns true, entered through <tt>scripts/shell</tt>). Usually,
23
+ # user code does not have to take the modes into account, but it is
24
+ # sometime useful.
25
+ #
26
+ # Finally, in both testing and runtime mode, the code can be started in
27
+ # simulation or live setups (see #simulation?). Specific plugins can for
28
+ # instance start and set up a simulation system in simulation mode, and as
29
+ # well set up some simulation-specific configuration for the functional
30
+ # layer of the architecture.
31
+ #
32
+ # == Configuration files
33
+ #
34
+ # In all modes, a specific set of configuration files are loaded. The
35
+ # files that are actually loaded are defined by the robot name and type, as
36
+ # specified to #robot. The loaded files are, in order, the following:
37
+ # [config/app.yml]
38
+ # the application configuration as a YAML file. See the comments in that
39
+ # file for more details.
40
+ # [config/init.rb]
41
+ # Ruby code for the common configuration of all robots
42
+ # [config/ROBOT_NAME.rb or config/ROBOT_TYPE.rb]
43
+ # Ruby code for the configuration of either all robots of the same type,
44
+ # or a specific robot. It is one or the other. If a given robot needs to
45
+ # inherit the configuration of its type, explicitely require the
46
+ # ROBOT_TYPE.rb file in config/ROBOT_NAME.rb.
47
+ #
48
+ # == Runtime mode (<tt>scripts/run</tt>)
49
+ # Then, in runtime mode the robot controller
50
+ # <tt>controller/ROBOT_NAME.rb</tt> or <tt>controller/ROBOT_TYPE.rb</tt> is
51
+ # loaded. The same rules than for the configuration file
52
+ # <tt>config/ROBOT_NAME.rb</tt> apply.
53
+ #
54
+ # == Testing mode (<tt>scripts/test</tt>)
55
+ # This mode is used to run test suites in the +test+ directory. See
56
+ # Roby::Test::TestCase for a description of Roby-specific tests.
57
+ class Application
58
+ include Singleton
59
+
60
+ # The plain option hash saved in config/app.yml
61
+ attr_reader :options
62
+
63
+ # Logging options.
64
+ # events:: save a log of all events in the system. This log can be read using scripts/replay
65
+ # If this value is 'stats', only the data necessary for timing statistics is saved.
66
+ # levels:: a component => level hash of the minimum level of the messages that
67
+ # should be displayed on the console. The levels are DEBUG, INFO, WARN and FATAL.
68
+ # Roby: FATAL
69
+ # Roby::Distributed: INFO
70
+ # dir:: the log directory. Uses APP_DIR/log if not set
71
+ # filter_backtraces:: true if the framework code should be removed from the error backtraces
72
+ attr_reader :log
73
+
74
+ # A [name, dir, file, module] array of available plugins, where 'name'
75
+ # is the plugin name, 'dir' the directory in which it is installed,
76
+ # 'file' the file which should be required to load the plugin and
77
+ # 'module' the Application-compatible module for configuration of the
78
+ # plug-in
79
+ attr_reader :available_plugins
80
+ # An [name, module] array of the loaded plugins
81
+ attr_reader :plugins
82
+
83
+ # The discovery options in multi-robot mode
84
+ attr_reader :discovery
85
+ # The robot's dRoby options
86
+ # period:: the period of neighbour discovery
87
+ # max_errors:: disconnect from a peer if there is more than +max_errors+ consecutive errors
88
+ # detected
89
+ attr_reader :droby
90
+
91
+ # Configuration of the control loop
92
+ # abort_on_exception:: if the control loop should abort if an uncaught task or event exception is received. Defaults
93
+ # to false
94
+ # abort_on_application_exception:: if the control should abort if an uncaught application exception (not originating
95
+ # from a task or event) is caught. Defaults to true.
96
+ attr_reader :control
97
+
98
+ # An array of directories in which to search for plugins
99
+ attr_reader :plugin_dirs
100
+
101
+ # True if user interaction is disabled during tests
102
+ attr_predicate :automatic_testing?, true
103
+
104
+ # True if all logs should be kept after testing
105
+ attr_predicate :testing_keep_logs?, true
106
+
107
+ # True if all logs should be kept after testing
108
+ attr_predicate :testing_overwrites_logs?, true
109
+
110
+ # True if we should remove the framework code from the error backtraces
111
+ def filter_backtraces?; log['filter_backtraces'] end
112
+ def filter_backtraces=(value); log['filter_backtraces'] = value end
113
+
114
+ def initialize
115
+ @plugins = Array.new
116
+ @available_plugins = Array.new
117
+ @log = Hash['events' => 'stats', 'levels' => Hash.new, 'filter_backtraces' => true]
118
+ @discovery = Hash.new
119
+ @droby = Hash['period' => 0.5, 'max_errors' => 1]
120
+ @control = Hash[ 'abort_on_exception' => false,
121
+ 'abort_on_application_exception' => true ]
122
+
123
+ @automatic_testing = true
124
+ @testing_keep_logs = false
125
+
126
+ @plugin_dirs = []
127
+ end
128
+
129
+ # Adds +dir+ in the list of directories searched for plugins
130
+ def plugin_dir(dir)
131
+ dir = File.expand_path(dir)
132
+ @plugin_dirs << dir
133
+ $LOAD_PATH.unshift File.expand_path(dir)
134
+
135
+ Dir.new(dir).each do |subdir|
136
+ subdir = File.join(dir, subdir)
137
+ next unless File.directory?(subdir)
138
+ appfile = File.join(subdir, "app.rb")
139
+ next unless File.file?(appfile)
140
+
141
+ begin
142
+ require appfile
143
+ rescue
144
+ Roby.warn "cannot load plugin in #{subdir}: #{$!.full_message}\n"
145
+ end
146
+ Roby.info "loaded plugin in #{subdir}"
147
+ end
148
+ end
149
+
150
+ # Returns true if +name+ is a loaded plugin
151
+ def loaded_plugin?(name)
152
+ plugins.any? { |plugname, _| plugname == name }
153
+ end
154
+
155
+ # Returns the [name, dir, file, module] array definition of the plugin
156
+ # +name+, or nil if +name+ is not a known plugin
157
+ def plugin_definition(name)
158
+ available_plugins.find { |plugname, *_| plugname == name }
159
+ end
160
+
161
+ # True if +name+ is a plugin known to us
162
+ def defined_plugin?(name)
163
+ available_plugins.any? { |plugname, *_| plugname == name }
164
+ end
165
+
166
+ def each_plugin(on_available = false)
167
+ plugins = self.plugins
168
+ if on_available
169
+ plugins = available_plugins.map { |name, _, mod, _| [name, mod] }
170
+ end
171
+ plugins.each do |_, mod|
172
+ yield(mod)
173
+ end
174
+ end
175
+
176
+ # Yields each extension modules that respond to +method+
177
+ def each_responding_plugin(method, on_available = false)
178
+ each_plugin do |mod|
179
+ yield(mod) if mod.respond_to?(method)
180
+ end
181
+ end
182
+
183
+ # Call +method+ on each loaded extension module which define it, with
184
+ # arguments +args+
185
+ def call_plugins(method, *args)
186
+ each_responding_plugin(method) do |config_extension|
187
+ config_extension.send(method, *args)
188
+ end
189
+ end
190
+
191
+ # Load configuration from the given option hash
192
+ def load_yaml(options)
193
+ options = options.dup
194
+
195
+ if robot_name && (robot_config = options['robots'])
196
+ if robot_config = robot_config[robot_name]
197
+ robot_config.each do |section, values|
198
+ if options[section]
199
+ options[section].merge! values
200
+ else
201
+ options[section] = values
202
+ end
203
+ end
204
+ options.delete('robots')
205
+ end
206
+ end
207
+
208
+ @options = options
209
+
210
+ load_option_hashes(options, %w{log control discovery droby})
211
+ call_plugins(:load, self, options)
212
+ end
213
+
214
+ def load_option_hashes(options, names)
215
+ names.each do |optname|
216
+ if options[optname]
217
+ send(optname).merge! options[optname]
218
+ end
219
+ end
220
+ end
221
+
222
+ # Loads the plugins whose name are listed in +names+
223
+ def using(*names)
224
+ names.each do |name|
225
+ name = name.to_s
226
+ unless plugin = plugin_definition(name)
227
+ raise ArgumentError, "#{name} is not a known plugin (#{available_plugins.map { |n, *_| n }.join(", ")})"
228
+ end
229
+ name, dir, mod, init = *plugin
230
+ if plugins.find { |n, m| n == name && m == mod }
231
+ next
232
+ end
233
+
234
+ if init
235
+ begin
236
+ $LOAD_PATH.unshift dir
237
+ init.call
238
+ mod.reset(self) if mod.respond_to?(:reset)
239
+ rescue Exception => e
240
+ Roby.fatal "cannot load plugin #{name}: #{e.full_message}"
241
+ exit(1)
242
+ ensure
243
+ $LOAD_PATH.shift
244
+ end
245
+ end
246
+
247
+ plugins << [name, mod]
248
+ extend mod
249
+ # If +load+ has already been called, call it on the module
250
+ if mod.respond_to?(:load) && options
251
+ mod.load(self, options)
252
+ end
253
+ end
254
+ end
255
+
256
+ def reset
257
+ if defined? State
258
+ State.clear
259
+ else
260
+ Roby.const_set(:State, StateSpace.new)
261
+ end
262
+ call_plugins(:reset, self)
263
+ end
264
+
265
+ # The robot name
266
+ attr_reader :robot_name
267
+ # The robot type
268
+ attr_reader :robot_type
269
+ # Sets up the name and type of the robot. This can be called only once
270
+ # in a given Roby controller.
271
+ def robot(name, type = name)
272
+ if @robot_name
273
+ if name != @robot_name && type != @robot_type
274
+ raise ArgumentError, "the robot is already set to #{name}, of type #{type}"
275
+ end
276
+ return
277
+ end
278
+ @robot_name = name
279
+ @robot_type = type
280
+ end
281
+
282
+ # The directory in which logs are to be saved
283
+ # Defaults to APP_DIR/log
284
+ def log_dir
285
+ File.expand_path(log['dir'] || 'log', APP_DIR)
286
+ end
287
+
288
+ # A path => File hash, to re-use the same file object for different
289
+ # logs
290
+ attribute(:log_files) { Hash.new }
291
+
292
+ # The directory in which results should be saved
293
+ # Defaults to APP_DIR/results
294
+ def results_dir
295
+ File.expand_path(log['results'] || 'results', APP_DIR)
296
+ end
297
+
298
+ # Returns a unique directory name as a subdirectory of
299
+ # +base_dir+, based on +path_spec+. The generated name
300
+ # is of the form
301
+ # <base_dir>/a/b/c/YYYYMMDD-basename
302
+ # if <tt>path_spec = "a/b/c/basename"</tt>. A .<number> suffix
303
+ # is appended if the path already exists.
304
+ def self.unique_dirname(base_dir, path_spec)
305
+ if path_spec =~ /\/$/
306
+ basename = ""
307
+ dirname = path_spec
308
+ else
309
+ basename = File.basename(path_spec)
310
+ dirname = File.dirname(path_spec)
311
+ end
312
+
313
+ date = Date.today
314
+ date = "%i%02i%02i" % [date.year, date.month, date.mday]
315
+ if basename && !basename.empty?
316
+ basename = date + "-" + basename
317
+ else
318
+ basename = date
319
+ end
320
+
321
+ # Check if +basename+ already exists, and if it is the case add a
322
+ # .x suffix to it
323
+ full_path = File.expand_path(File.join(dirname, basename), base_dir)
324
+ base_dir = File.dirname(full_path)
325
+
326
+ unless File.exists?(base_dir)
327
+ FileUtils.mkdir_p(base_dir)
328
+ end
329
+
330
+ final_path, i = full_path, 0
331
+ while File.exists?(final_path)
332
+ i += 1
333
+ final_path = full_path + ".#{i}"
334
+ end
335
+
336
+ final_path
337
+ end
338
+
339
+ # Sets up all the default loggers. It creates the logger for the Robot
340
+ # module (accessible through Robot.logger), and sets up log levels as
341
+ # specified in the <tt>config/app.yml</tt> file.
342
+ def setup_loggers
343
+ # Create the robot namespace
344
+ STDOUT.sync = true
345
+ Robot.logger = Logger.new(STDOUT)
346
+ Robot.logger.level = Logger::INFO
347
+ Robot.logger.formatter = Roby.logger.formatter
348
+ Robot.logger.progname = robot_name
349
+
350
+ # Set up log levels
351
+ log['levels'].each do |name, value|
352
+ name = name.camelize
353
+ if value =~ /^(\w+):(.+)$/
354
+ level, file = $1, $2
355
+ level = Logger.const_get(level)
356
+ file = file.gsub('ROBOT', robot_name) if robot_name
357
+ else
358
+ level = Logger.const_get(value)
359
+ end
360
+
361
+ new_logger = if file
362
+ path = File.expand_path(file, log_dir)
363
+ io = (log_files[path] ||= File.open(path, 'w'))
364
+ Logger.new(io)
365
+ else Logger.new(STDOUT)
366
+ end
367
+ new_logger.level = level
368
+ new_logger.formatter = Roby.logger.formatter
369
+
370
+ if (mod = name.constantize rescue nil)
371
+ if robot_name
372
+ new_logger.progname = "#{name} #{robot_name}"
373
+ else
374
+ new_logger.progname = name
375
+ end
376
+ mod.logger = new_logger
377
+ end
378
+ end
379
+ end
380
+
381
+ def setup_dirs
382
+ Dir.mkdir(log_dir) unless File.exists?(log_dir)
383
+ if File.directory?(libdir = File.join(APP_DIR, 'lib'))
384
+ if !$LOAD_PATH.include?(libdir)
385
+ $LOAD_PATH.unshift File.join(APP_DIR, 'lib')
386
+ end
387
+ end
388
+
389
+ Roby::State.datadirs = []
390
+ datadir = File.join(APP_DIR, "data")
391
+ if File.directory?(datadir)
392
+ Roby::State.datadirs << datadir
393
+ end
394
+ end
395
+
396
+ # Loads the models, based on the given robot name and robot type
397
+ def require_models
398
+ # Require all common task models and the task models specific to
399
+ # this robot
400
+ require_dir(File.join(APP_DIR, 'tasks'))
401
+ require_robotdir(File.join(APP_DIR, 'tasks', 'ROBOT'))
402
+
403
+ # Load robot-specific configuration
404
+ planner_dir = File.join(APP_DIR, 'planners')
405
+ models_search = [planner_dir]
406
+ if robot_name
407
+ load_robotfile(File.join(APP_DIR, 'config', "ROBOT.rb"))
408
+
409
+ models_search << File.join(planner_dir, robot_name) << File.join(planner_dir, robot_type)
410
+ if !require_robotfile(File.join(APP_DIR, 'planners', 'ROBOT', 'main.rb'))
411
+ require File.join(APP_DIR, "planners", "main")
412
+ end
413
+ else
414
+ require File.join(APP_DIR, "planners", "main")
415
+ end
416
+
417
+ # Load the other planners
418
+ models_search.each do |base_dir|
419
+ next unless File.directory?(base_dir)
420
+ Dir.new(base_dir).each do |file|
421
+ if File.file?(file) && file =~ /\.rb$/ && file !~ 'main\.rb$'
422
+ require file
423
+ end
424
+ end
425
+ end
426
+ end
427
+
428
+ def setup
429
+ reset
430
+
431
+ $LOAD_PATH.unshift(APP_DIR) unless $LOAD_PATH.include?(APP_DIR)
432
+
433
+ # Get the application-wide configuration
434
+ file = File.join(APP_DIR, 'config', 'app.yml')
435
+ file = YAML.load(File.open(file))
436
+ load_yaml(file)
437
+ if File.exists?(initfile = File.join(APP_DIR, 'config', 'init.rb'))
438
+ load initfile
439
+ end
440
+
441
+ setup_dirs
442
+ setup_loggers
443
+
444
+ # Import some constants directly at toplevel before loading the
445
+ # user-defined models
446
+ unless Object.const_defined?(:Application)
447
+ Object.const_set(:Application, Roby::Application)
448
+ Object.const_set(:State, Roby::State)
449
+ end
450
+
451
+ require_models
452
+
453
+ # MainPlanner is always included in the planner list
454
+ Roby.control.planners << MainPlanner
455
+
456
+ # Set up the loaded plugins
457
+ call_plugins(:setup, self)
458
+
459
+ # If we are in test mode, import the test extensions from plugins
460
+ if testing?
461
+ require 'roby/test/testcase'
462
+ each_plugin do |mod|
463
+ if mod.const_defined?(:Test)
464
+ Roby::Test::TestCase.include mod.const_get(:Test)
465
+ end
466
+ end
467
+ end
468
+ end
469
+
470
+ def run(&block)
471
+ # Set up dRoby, setting an Interface object as front server, for shell access
472
+ host = droby['host'] || ""
473
+ if host !~ /:\d+$/
474
+ host << ":#{Distributed::DEFAULT_DROBY_PORT}"
475
+ end
476
+
477
+ if single? || !robot_name
478
+ host =~ /:(\d+)$/
479
+ DRb.start_service "druby://:#{$1 || '0'}", Interface.new(Roby.control)
480
+ else
481
+ DRb.start_service "druby://#{host}", Interface.new(Roby.control)
482
+ droby_config = { :ring_discovery => !!discovery['ring'],
483
+ :name => robot_name,
484
+ :plan => Roby.plan,
485
+ :period => discovery['period'] || 0.5 }
486
+
487
+ if discovery['tuplespace']
488
+ droby_config[:discovery_tuplespace] = DRbObject.new_with_uri("druby://#{discovery['tuplespace']}")
489
+ end
490
+ Roby::Distributed.state = Roby::Distributed::ConnectionSpace.new(droby_config)
491
+
492
+ if discovery['ring']
493
+ Roby::Distributed.publish discovery['ring']
494
+ end
495
+ Roby::Control.every(discovery['period'] || 0.5) do
496
+ Roby::Distributed.state.start_neighbour_discovery
497
+ end
498
+ end
499
+
500
+ @robot_name ||= 'common'
501
+ @robot_type ||= 'common'
502
+
503
+ control_config = self.control
504
+ control = Roby.control
505
+ options = { :detach => true, :cycle => control_config['cycle'] || 0.1 }
506
+
507
+ # Add an executive if one is defined
508
+ if control_config['executive']
509
+ self.executive = control_config['executive']
510
+ end
511
+
512
+ if log['events']
513
+ require 'roby/log/file'
514
+ logfile = File.join(log_dir, robot_name)
515
+ logger = Roby::Log::FileLogger.new(logfile)
516
+ logger.stats_mode = log['events'] == 'stats'
517
+ Roby::Log.add_logger logger
518
+ end
519
+ control.abort_on_exception =
520
+ control_config['abort_on_exception']
521
+ control.abort_on_application_exception =
522
+ control_config['abort_on_application_exception']
523
+ control.run options
524
+
525
+ plugins = self.plugins.map { |_, mod| mod if mod.respond_to?(:run) }.compact
526
+ run_plugins(plugins, &block)
527
+
528
+ rescue Exception => e
529
+ if e.respond_to?(:pretty_print)
530
+ pp e
531
+ else
532
+ pp e.full_message
533
+ end
534
+ end
535
+ def run_plugins(mods, &block)
536
+ control = Roby.control
537
+
538
+ if mods.empty?
539
+ yield
540
+ control.join
541
+ else
542
+ mod = mods.shift
543
+ mod.run(self) do
544
+ run_plugins(mods, &block)
545
+ end
546
+ end
547
+
548
+ rescue Exception => e
549
+ if Roby.control.running?
550
+ control.quit
551
+ control.join
552
+ raise e, e.message, e.backtrace
553
+ else
554
+ raise
555
+ end
556
+ end
557
+
558
+ attr_reader :executive
559
+
560
+ def executive=(name)
561
+ if executive
562
+ Control.event_processing.delete(executive.method(:initial_events))
563
+ @executive = nil
564
+ end
565
+ return unless name
566
+
567
+ full_name = "roby/executives/#{name}"
568
+ require full_name
569
+ @executive = full_name.camelize.constantize.new
570
+ Control.event_processing << executive.method(:initial_events)
571
+ end
572
+
573
+ def stop; call_plugins(:stop, self) end
574
+
575
+ DISCOVERY_TEMPLATE = [:droby, nil, nil]
576
+
577
+ # Starts services needed for distributed operations. These services are
578
+ # supposed to be started only once for a whole system
579
+ #
580
+ # If you have external servers to start for every robot, plug it into
581
+ # #start_server
582
+ def start_distributed
583
+ Thread.abort_on_exception = true
584
+
585
+ if !File.exists?(log_dir)
586
+ Dir.mkdir(log_dir)
587
+ end
588
+
589
+ unless single? || !discovery['tuplespace']
590
+ ts = Rinda::TupleSpace.new
591
+
592
+
593
+ discovery['tuplespace'] =~ /(:\d+)$/
594
+ DRb.start_service "druby://#{$1}", ts
595
+
596
+ new_db = ts.notify('write', DISCOVERY_TEMPLATE)
597
+ take_db = ts.notify('take', DISCOVERY_TEMPLATE)
598
+
599
+ Thread.start do
600
+ new_db.each { |_, t| STDERR.puts "new host #{t[1]}" }
601
+ end
602
+ Thread.start do
603
+ take_db.each { |_, t| STDERR.puts "host #{t[1]} has disconnected" }
604
+ end
605
+ Roby.warn "Started service discovery on #{discovery['tuplespace']}"
606
+ end
607
+
608
+ call_plugins(:start_distributed, self)
609
+ end
610
+
611
+ # Stop services needed for distributed operations. See #start_distributed
612
+ def stop_distributed
613
+ DRb.stop_service
614
+
615
+ call_plugins(:stop_distributed, self)
616
+ rescue Interrupt
617
+ end
618
+
619
+ attr_reader :log_server
620
+ attr_reader :log_sources
621
+
622
+ # Start services that should exist for every robot in the system. Services that
623
+ # are needed only once for all robots should be started in #start_distributed
624
+ def start_server
625
+ Thread.abort_on_exception = true
626
+
627
+ # Start a log server if needed, and poll the log directory for new
628
+ # data sources
629
+ if log_server = (log.has_key?('server') ? log['server'] : true)
630
+ require 'roby/log/server'
631
+ port = if log_server.kind_of?(Hash) && log_server['port']
632
+ Integer(log_server['port'])
633
+ end
634
+
635
+ @log_server = Log::Server.new(port ||= Log::Server::RING_PORT)
636
+ Roby::Log::Server.info "log server published on port #{port}"
637
+ @log_streams = []
638
+ @log_streams_poll = Thread.new do
639
+ begin
640
+ loop do
641
+ Thread.exclusive do
642
+ known_streams = @log_server.streams
643
+ streams = data_streams
644
+
645
+ (streams - known_streams).each do |s|
646
+ Roby::Log::Server.info "new stream found #{s.name} [#{s.type}]"
647
+ s.open
648
+ @log_server.added_stream(s)
649
+ end
650
+ (known_streams - streams).each do |s|
651
+ Roby::Log::Server.info "end of stream #{s.name} [#{s.type}]"
652
+ s.close
653
+ @log_server.removed_stream(s)
654
+ end
655
+ end
656
+ sleep(5)
657
+ end
658
+ rescue Interrupt
659
+ rescue
660
+ Roby::Log::Server.fatal $!.full_message
661
+ end
662
+ end
663
+ end
664
+
665
+ call_plugins(:start_server, self)
666
+ end
667
+
668
+ # Stop server. See #start_server
669
+ def stop_server
670
+ if @log_server
671
+ @log_streams_poll.raise Interrupt, "quitting"
672
+ @log_streams_poll.join
673
+
674
+ @log_server.quit
675
+ @log_streams.clear
676
+ end
677
+
678
+ call_plugins(:stop_server, self)
679
+ end
680
+
681
+ # Require all files in +dirname+
682
+ def require_dir(dirname)
683
+ Dir.new(dirname).each do |file|
684
+ file = File.join(dirname, file)
685
+ file = file.gsub(/^#{Regexp.quote(APP_DIR)}\//, '')
686
+ require file if file =~ /\.rb$/ && File.file?(file)
687
+ end
688
+ end
689
+
690
+ # Require all files in the directories matching +pattern+. If +pattern+
691
+ # contains the word ROBOT, it is replaced by -- in order -- the robot
692
+ # name and then the robot type
693
+ def require_robotdir(pattern)
694
+ return unless robot_name && robot_type
695
+
696
+ [robot_name, robot_type].each do |name|
697
+ dirname = pattern.gsub(/ROBOT/, name)
698
+ require_dir(dirname) if File.directory?(dirname)
699
+ end
700
+ end
701
+
702
+ # Loads the first file found matching +pattern+
703
+ #
704
+ # See #require_robotfile
705
+ def load_robotfile(pattern)
706
+ require_robotfile(pattern, :load)
707
+ end
708
+
709
+ # Requires or loads (according to the value of +method+) the first file
710
+ # found matching +pattern+. +pattern+ can contain the word ROBOT, in
711
+ # which case the file is first checked against the robot name and then
712
+ # against the robot type
713
+ def require_robotfile(pattern, method = :require)
714
+ return unless robot_name && robot_type
715
+
716
+ robot_config = pattern.gsub(/ROBOT/, robot_name)
717
+ if File.file?(robot_config)
718
+ Kernel.send(method, robot_config)
719
+ true
720
+ else
721
+ robot_config = pattern.gsub(/ROBOT/, robot_type)
722
+ if File.file?(robot_config)
723
+ Kernel.send(method, robot_config)
724
+ true
725
+ else
726
+ false
727
+ end
728
+ end
729
+ end
730
+
731
+ attr_predicate :simulation?, true
732
+ def simulation; self.simulation = true end
733
+
734
+ attr_predicate :testing?, true
735
+ def testing; self.testing = true end
736
+ attr_predicate :shell?, true
737
+ def shell; self.shell = true end
738
+ def single?; @single || discovery.empty? end
739
+ def single; @single = true end
740
+
741
+ # Guesses the type of +filename+ if it is a source suitable for
742
+ # data display in this application
743
+ def data_streams_of(filenames)
744
+ if filenames.size == 1
745
+ path = filenames.first
746
+ path = if path =~ /-(events|timings)\.log$/
747
+ $`
748
+ elsif File.exists?("#{path}-events.log")
749
+ path
750
+ end
751
+ if path
752
+ return [Roby::Log::EventStream.new(path)]
753
+ end
754
+ end
755
+
756
+ each_responding_plugin(:data_streams_of, true) do |config|
757
+ if streams = config.data_streams_of(filenames)
758
+ return streams
759
+ end
760
+ end
761
+ nil
762
+ end
763
+
764
+ # Returns the list of data streams suitable for data display known
765
+ # to the application
766
+ def data_streams(log_dir = nil)
767
+ log_dir ||= self.log_dir
768
+ streams = []
769
+ Dir.glob(File.join(log_dir, '*-events.log*')).each do |file|
770
+ next unless file =~ /-events\.log$/
771
+ streams << Roby::Log::EventStream.new($`)
772
+ end
773
+ each_responding_plugin(:data_streams, true) do |config|
774
+ if s = config.data_streams(log_dir)
775
+ streams += s
776
+ end
777
+ end
778
+ streams
779
+ end
780
+
781
+ def self.find_data(name)
782
+ Roby::State.datadirs.each do |dir|
783
+ path = File.join(dir, name)
784
+ return path if File.exists?(path)
785
+ end
786
+ raise Errno::ENOENT, "no file #{name} found in #{Roby::State.datadirs.join(":")}"
787
+ end
788
+
789
+ def self.register_plugin(name, mod, &init)
790
+ caller(1)[0] =~ /^([^:]+):\d/
791
+ dir = File.expand_path(File.dirname($1))
792
+ Roby.app.available_plugins << [name, dir, mod, init]
793
+ end
794
+
795
+ @@reload_model_filter = []
796
+ # Add a filter to model reloading. A task or planner model is
797
+ # reinitialized only if all filter blocks return true for it
798
+ def self.filter_reloaded_models(&block)
799
+ @@reload_model_filter << block
800
+ end
801
+
802
+ def model?(model)
803
+ (model <= Roby::Task) || (model.kind_of?(Roby::TaskModelTag)) ||
804
+ (model <= Planning::Planner) || (model <= Planning::Library)
805
+ end
806
+
807
+ def reload_model?(model)
808
+ @@reload_model_filter.all? { |filter| filter[model] }
809
+ end
810
+
811
+ def app_file?(path)
812
+ (path =~ %r{(^|/)#{APP_DIR}(/|$)}) ||
813
+ ((path[0] != ?/) && File.file?(File.join(APP_DIR, path)))
814
+ end
815
+ def framework_file?(path)
816
+ if path =~ /roby\/.*\.rb$/
817
+ true
818
+ else
819
+ Roby.app.plugins.any? do |name, _|
820
+ _, dir, _, _ = Roby.app.plugin_definition(name)
821
+ path =~ %r{(^|/)#{dir}(/|$)}
822
+ end
823
+ end
824
+ end
825
+
826
+ def reload
827
+ # Always reload this file first. This ensure that one can use #reload
828
+ # to fix the reload code itself
829
+ load __FILE__
830
+
831
+ # Clear all event definitions in task models that are filtered out by
832
+ # Application.filter_reloaded_models
833
+ ObjectSpace.each_object(Class) do |model|
834
+ next unless model?(model)
835
+ next unless reload_model?(model)
836
+
837
+ model.clear_model
838
+ end
839
+
840
+ # Remove what we want to reload from LOADED_FEATURES and use
841
+ # require. Do not use 'load' as the reload order should be the
842
+ # require order.
843
+ needs_reload = []
844
+ $LOADED_FEATURES.delete_if do |feature|
845
+ if framework_file?(feature) || app_file?(feature)
846
+ needs_reload << feature
847
+ end
848
+ end
849
+
850
+ needs_reload.each do |feature|
851
+ begin
852
+ require feature.gsub(/\.rb$/, '')
853
+ rescue Exception => e
854
+ STDERR.puts e.full_message
855
+ end
856
+ end
857
+ end
858
+ end
859
+
860
+ # Load the plugins 'main' files
861
+ Roby.app.plugin_dir File.join(ROBY_ROOT_DIR, 'plugins')
862
+ if plugin_path = ENV['ROBY_PLUGIN_PATH']
863
+ plugin_path.split(':').each do |dir|
864
+ if File.directory?(dir)
865
+ Roby.app.plugin_dir File.expand_path(dir)
866
+ end
867
+ end
868
+ end
869
+ end
870
+