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
@@ -0,0 +1,13 @@
1
+ require 'roby'
2
+
3
+ module Roby
4
+ module Planning
5
+ extend Logger::Hierarchy
6
+ extend Logger::Forward
7
+ end
8
+ end
9
+
10
+ require 'roby/planning/task'
11
+ require 'roby/planning/loops'
12
+ require 'roby/planning/model'
13
+
@@ -0,0 +1,302 @@
1
+ require 'roby/planning/task'
2
+
3
+ module Roby
4
+ # This class unrolls a loop in the plan. It maintains +lookahead+ patterns
5
+ # developped at all times by calling an external planner, and manages them.
6
+ # This documentation will start by describing the general behaviour of this
7
+ # task, and then we will detail different specific modes of operation.
8
+ #
9
+ # == Behaviour description
10
+ # The task unrolls the loop by generating /patterns/, which are a
11
+ # combination of a task representing the operation to be done during one
12
+ # pass of the loop, and a planning task which will generate the subplan for
13
+ # this operation. These patterns are developped as children of either the
14
+ # PlanningLoop task itself, or its planned_task if there is one.
15
+ #
16
+ # During the execution of this suite of patterns, the following constraints
17
+ # are always met:
18
+ #
19
+ # * the planning task of a pattern is started after the one of the previous
20
+ # pattern has finished.
21
+ # * a pattern is started after the previous one has finished.
22
+ #
23
+ # The #start! command do not starts the loop per-se. It only makes the
24
+ # first +lookahead+ patterns to be developped. You have to call
25
+ # #loop_start! once to start the generated patterns themselves.
26
+ #
27
+ # == Periodic and nonperiodic loops
28
+ # On the one hand, if the +:period+ option of #initialize is non-nil, it is
29
+ # expected to be a floating-point value representing a time in seconds. In
30
+ # that case, the loop is *periodic* and each pattern in the loop is started
31
+ # at the given periodic rate, triggered by the #periodic_trigger event.
32
+ # Note that the 'zero-period' case is a special situation where the loop
33
+ # runs as fast as possible.
34
+ #
35
+ # On the other hand, if +:period+ is nil, the loop is nonperiodic, and each
36
+ # pattern must be explicitely started by calling #loop_start!. Finally,
37
+ # #loop_start! can also be called to bypass the period value (i.e. to
38
+ # start a pattern earlier than expected). Repetitive calls to #loop_start!
39
+ # will make the loop develop and start at most one pattern.
40
+ #
41
+ # == Zero lookahead
42
+ # When the loop lookahead is nonzero, patterns are planend ahead-of-time: they
43
+ # are planned as soon as possible. In some cases, it is non desirable, for instance
44
+ # because some information is available only at a later time.
45
+ #
46
+ # For these situations, one can use a zero lookahead. In that case, the
47
+ # patterns are not pre-planned, but instead the planning task is started
48
+ # only when the pattern itself should have been started: either when the
49
+ # period timeouts, or when #loop_start! is explicitely called.
50
+ #
51
+ # TODO: make figures.
52
+ #
53
+ class PlanningLoop < Roby::Task
54
+ terminates
55
+
56
+ # An array of [planning_task, user_command]. The *last* element is the
57
+ # *first* arrived
58
+ attr_reader :patterns
59
+
60
+ # For periodic updates. If false, the next loop is started when the
61
+ # 'loop_start' command is called
62
+ argument :period
63
+ # How many loops should we have unrolled at all times
64
+ argument :lookahead
65
+
66
+ # The task model we should produce
67
+ argument :planned_model
68
+
69
+ # The planner model we should use
70
+ argument :planner_model
71
+ # The planner method name
72
+ argument :method_name
73
+ # The planner method options
74
+ argument :method_options
75
+
76
+ # Filters the options in +options+, splitting between the options that
77
+ # are specific to the planning task and those that are to be forwarded
78
+ # to the planner itself
79
+ def self.filter_options(options) # :nodoc:
80
+ task_arguments, planning_options = Kernel.filter_options options,
81
+ :period => nil,
82
+ :lookahead => 1,
83
+ :planner_model => nil,
84
+ :planned_model => Roby::Task,
85
+ :method_name => nil,
86
+ :method_options => {},
87
+ :planning_owners => nil
88
+
89
+ if !task_arguments[:method_name]
90
+ raise ArgumentError, "required argument :method_name missing"
91
+ elsif !task_arguments[:planner_model]
92
+ raise ArgumentError, "required argument :planner_model missing"
93
+ elsif task_arguments[:lookahead] < 0
94
+ raise ArgumentError, "lookahead must be positive"
95
+ end
96
+ task_arguments[:period] ||= nil
97
+ [task_arguments, planning_options]
98
+ end
99
+
100
+ # If this loop is periodic of nonzero period, the state event which
101
+ # represents that period.
102
+ attr_reader :periodic_trigger
103
+
104
+ def initialize(options)
105
+ task_arguments, planning_options = PlanningLoop.filter_options(options)
106
+ task_arguments[:method_options].merge!(planning_options)
107
+ super(task_arguments)
108
+
109
+ if period && period > 0
110
+ @periodic_trigger = State.on_delta :t => period
111
+ periodic_trigger.disable
112
+ periodic_trigger.on event(:loop_start)
113
+ end
114
+
115
+ @patterns = []
116
+ @pattern_id = 0
117
+ end
118
+
119
+ # The task on which the children are added
120
+ def main_task; planned_task || self end
121
+
122
+ def planned_task # :nodoc:
123
+ planned_tasks.find { true }
124
+ end
125
+
126
+ # The PlanningTask object for the last pattern
127
+ def last_planning_task
128
+ if pattern = patterns.first
129
+ pattern.first
130
+ end
131
+ end
132
+
133
+ # Appends a new unplanned pattern after all the patterns already developped
134
+ #
135
+ # +context+ is forwarded to the planned task
136
+ def append_pattern(*context)
137
+ # Create the new pattern
138
+ task_arguments = arguments.slice(:planner_model, :planned_model, :method_name)
139
+ task_arguments[:method_options] = method_options.dup
140
+ task_arguments[:method_options][:pattern_id] = @pattern_id
141
+ @pattern_id += 1
142
+
143
+ planning = PlanningTask.new(task_arguments)
144
+ planned = planning.planned_task
145
+ planned.forward(:start, self, :loop_start)
146
+ planned.forward(:success, self, :loop_success)
147
+ planned.forward(:stop, self, :loop_end)
148
+ main_task.realized_by planned
149
+
150
+ # Schedule it. We start the new pattern when these three conditions are met:
151
+ # * it has been planned (planning has finished)
152
+ # * the previous one (if any) has finished
153
+ # * the period (if any) has expired or an external event required
154
+ # the explicit start of the pattern (call done to user_command,
155
+ # for instance through a call to #loop_start!)
156
+ #
157
+ # The +precondition+ event represents a situation where the new pattern
158
+ # *can* be started, while +command+ is the situation asking for the
159
+ # pattern to start.
160
+ precondition = planning.event(:success)
161
+ user_command = EventGenerator.new(true)
162
+ command = user_command
163
+
164
+ if last_planning = last_planning_task
165
+ last_planned = last_planning.planned_task
166
+
167
+ if !last_planned.finished?
168
+ precondition &= last_planned.event(:stop)
169
+ end
170
+
171
+ if period && !periodic_trigger
172
+ command |= planned.event(:success)
173
+ end
174
+
175
+ if last_planning.finished?
176
+ planning.start!(*context)
177
+ else
178
+ last_planning.event(:success).filter(*context).on(planning.event(:start))
179
+ end
180
+ end
181
+ command &= precondition
182
+
183
+ patterns.unshift([planning, user_command])
184
+ command.on(planned.event(:start))
185
+ planning
186
+ end
187
+
188
+ # Remove all pending patterns and starts unrolling as much new patterns
189
+ # as lookahead requires. Kills the currently running pattern (if there
190
+ # is one).
191
+ event :reinit do |context|
192
+ unless running?
193
+ raise ArgumentError, "#reinit called, but the loop is not running"
194
+ end
195
+
196
+ did_reinit = []
197
+
198
+ # Remove all realized_by relations and all pending patterns from
199
+ # the pattern set.
200
+ for pattern in patterns
201
+ old_planning, ev = pattern
202
+ old_task = old_planning.planned_task
203
+ main_task.remove_child old_task
204
+
205
+ if old_task && old_task.running?
206
+ did_reinit << old_task.event(:stop)
207
+ elsif old_planning.running?
208
+ did_reinit << old_planning.event(:stop)
209
+ end
210
+ end
211
+ patterns.clear
212
+
213
+ if did_reinit.empty?
214
+ emit :reinit
215
+ else
216
+ did_reinit.
217
+ map { |ev| ev.when_unreachable }.
218
+ inject { |a, b| a & b }.
219
+ forward event(:reinit)
220
+ end
221
+ end
222
+ on :reinit do |ev|
223
+ @pattern_id = 0
224
+ if lookahead > 0
225
+ first_planning = nil
226
+ while patterns.size < lookahead
227
+ new_planning = append_pattern
228
+ first_planning ||= new_planning
229
+ end
230
+ first_planning.start!
231
+ end
232
+ loop_start!
233
+ end
234
+
235
+ # Generates the first +lookahead+ patterns and start planning. The
236
+ # patterns themselves are started when +loop_start+ is called the first
237
+ # time.
238
+ event :start do
239
+ if lookahead > 0
240
+ first_planning = nil
241
+ while patterns.size < lookahead
242
+ new_planning = append_pattern
243
+ first_planning ||= new_planning
244
+ end
245
+ on(:start, first_planning)
246
+ end
247
+
248
+ emit :start
249
+ end
250
+
251
+
252
+ # The first time, start executing the patterns. During the loop
253
+ # execution, force starting the next pending pattern, bypassing the
254
+ # period if there is one. In case of zero-lookahead loops, the next
255
+ # pattern will be planned before it is executed.
256
+ event :loop_start do |context|
257
+ # Start the periodic trigger if there is one
258
+ if periodic_trigger && periodic_trigger.disabled?
259
+ periodic_trigger.enable
260
+ end
261
+
262
+ # Find the first non-running pattern and start it. In case of
263
+ # zero-lookahead, if no task is already pending, we should add one
264
+ # and start it explicitely
265
+ if new_pattern = patterns.reverse.find { |task, ev| task.planned_task.pending? }
266
+ t, ev = new_pattern
267
+ ev.call(*context)
268
+ command = ev.enum_child_objects(EventStructure::Signal).find { true }
269
+ elsif lookahead == 0
270
+ start_planning = !last_planning_task
271
+ planning = append_pattern(*context)
272
+ if start_planning
273
+ planning.start!(*context)
274
+ end
275
+ _, ev = patterns[0]
276
+ ev.call(*context)
277
+ end
278
+ end
279
+
280
+ on :loop_start do |event|
281
+ return unless self_owned?
282
+ if event.task.lookahead != 0
283
+ append_pattern
284
+ end
285
+
286
+ main_task.remove_finished_children
287
+ end
288
+
289
+ event :loop_success
290
+
291
+ event :loop_end
292
+ on :loop_end do |event|
293
+ return unless self_owned?
294
+ patterns.pop
295
+ end
296
+
297
+ # For ordering during event propagation
298
+ causal_link :loop_start => :loop_end
299
+ causal_link :loop_success => :loop_end
300
+ end
301
+ end
302
+
@@ -0,0 +1,906 @@
1
+ require 'roby/planning/task'
2
+ require 'roby/task'
3
+ require 'roby/control'
4
+ require 'roby/plan'
5
+ require 'utilrb/module/ancestor_p'
6
+ require 'set'
7
+
8
+ module Roby
9
+ # The Planning module provides basic tools to create plans (graph of tasks
10
+ # and events)
11
+ module Planning
12
+ # Violation of plan models, for instance if a method returns a Task object
13
+ # which is of a wrong model
14
+ class PlanModelError < RuntimeError
15
+ attr_accessor :planner
16
+ def initialize(planner = nil)
17
+ @planner = planner
18
+ super()
19
+ end
20
+ end
21
+
22
+ # Raised a method has found no valid development
23
+ class NotFound < PlanModelError
24
+ # The name of the method which has failed
25
+ attr_accessor :method_name
26
+ # The planning options
27
+ attr_accessor :method_options
28
+ # A method => error hash of all the method that have
29
+ # been tried. +error+ can either be a NotFound exception
30
+ # or another exception
31
+ attr_reader :errors
32
+
33
+ def initialize(planner, errors)
34
+ @errors = errors
35
+ super(planner)
36
+ end
37
+
38
+ def message
39
+ if errors.empty?
40
+ "no candidate for #{method_name}(#{method_options})"
41
+ else
42
+ msg = "cannot develop a #{method_name}(#{method_options}) method"
43
+ first, *rem = *Roby.filter_backtrace(backtrace)
44
+
45
+ full = "#{first}: #{msg}\n from #{rem.join("\n from ")}"
46
+ errors.each do |m, error|
47
+ first, *rem = *Roby.filter_backtrace(error.backtrace)
48
+ full << "\n#{first}: #{m} failed with #{error.message}\n from #{rem.join("\n from ")}"
49
+ end
50
+ full
51
+ end
52
+ end
53
+
54
+ def full_message
55
+ msg = message
56
+ first, *rem = *Roby.filter_backtrace(backtrace)
57
+
58
+ full = "#{first}: #{msg}\n from #{rem.join("\n from ")}"
59
+ errors.each do |m, error|
60
+ first = error.backtrace.first
61
+ full << "\n#{first} #{m} failed because of #{error.full_message}"
62
+ end
63
+ full
64
+ end
65
+ end
66
+
67
+ # Some common tools for Planner and Library
68
+ module Tools
69
+ def using(*modules)
70
+ modules.each do |mod|
71
+ if mod.respond_to?(:planning_methods)
72
+ include mod
73
+ elsif planning_mod = (mod.const_get('Planning') rescue nil)
74
+ include planning_mod
75
+ else
76
+ raise ArgumentError, "#{mod} is not a planning library and has no Planning module which is one"
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ # This mixin defines the method inheritance validation method. This is
83
+ # then used by MethodDefinition and MethodModel
84
+ module MethodInheritance
85
+ # Checks that options in +options+ can be used to overload +self+.
86
+ # Updates options if needed
87
+ def validate(options)
88
+ if returns
89
+ if options[:returns] && !(options[:returns] <= returns)
90
+ raise ArgumentError, "return task type #{options[:returns]} forbidden since it overloads #{returns}"
91
+ else
92
+ options[:returns] ||= returns
93
+ end
94
+ end
95
+
96
+ if self.options.has_key?(:reuse)
97
+ if options.has_key?(:reuse) && options[:reuse] != self.options[:reuse]
98
+ raise ArgumentError, "the :reuse option is already set on the #{name} model"
99
+ end
100
+ options[:reuse] = self.options[:reuse]
101
+ else
102
+ options[:reuse] = true unless options.has_key?(:reuse)
103
+ end
104
+
105
+ options
106
+ end
107
+ end
108
+
109
+ # An implementation of a planning method.
110
+ class MethodDefinition
111
+ include MethodInheritance
112
+
113
+ attr_reader :name, :options, :body
114
+ def initialize(name, options, body)
115
+ @name, @options, @body = name, options, body
116
+ end
117
+
118
+ # The method ID
119
+ def id; options[:id] end
120
+ # If this method handles recursion
121
+ def recursive?; options[:recursive] end
122
+ # What kind of task this method returns
123
+ #
124
+ # If this is nil, the method may return a task array or a task
125
+ # aggregation
126
+ def returns; options[:returns] end
127
+ # If the method allows reusing tasks already in the plan
128
+ # reuse? is always false if there is no return type defined
129
+ def reuse?; (!options.has_key?(:reuse) || options[:reuse]) if returns end
130
+ # Call the method definition
131
+ def call(planner); body.call(planner) end
132
+
133
+ def to_s; "#{name}:#{id}(#{options})" end
134
+ end
135
+
136
+ # The model of a planning method. This does not define an actual
137
+ # implementation of the method, only the model methods should abide to.
138
+ class MethodModel
139
+ include MethodInheritance
140
+
141
+ # The return type the method model defines
142
+ #
143
+ # If this is nil, methods of this model may return a task array
144
+ # or a task aggregation
145
+ def returns; options[:returns] end
146
+ # If the model allows reusing tasks already in the plan
147
+ def reuse?; !options.has_key?(:reuse) || options[:reuse] end
148
+
149
+ # The model name
150
+ attr_reader :name
151
+ # The model options, as a Hash
152
+ attr_reader :options
153
+
154
+ def initialize(name, options = Hash.new); @name, @options = name, options end
155
+ def ==(model)
156
+ name == model.name && options == model.options
157
+ end
158
+
159
+ # call-seq:
160
+ # merge(new_options) => self
161
+ #
162
+ # Add new options in this model. Raises ArgumentError if the
163
+ # new options cannot be merged because they are incompatible
164
+ # with the current model definition
165
+ def merge(new_options)
166
+ validate_options(new_options, [:returns, :reuse])
167
+ validate_option(new_options, :returns, false) do |rettype|
168
+ if options[:returns] && options[:returns] != rettype
169
+ raise ArgumentError, "return type already specified for method #{name}"
170
+ end
171
+ options[:returns] = rettype
172
+ end
173
+ validate_option(new_options, :reuse, false) do |flag|
174
+ if options.has_key?(:reuse) && options[:reuse] != flag
175
+ raise ArgumentError, "the reuse flag is already set to #{options[:reuse]} on #{name}"
176
+ end
177
+ options[:reuse] = flag
178
+ true
179
+ end
180
+
181
+ self
182
+ end
183
+
184
+ def overload(old_model)
185
+ if old_returns = old_model.returns
186
+ if returns && !(returns < old_returns)
187
+ raise ArgumentError, "new return type #{returns} is not a subclass of the old one #{old_returns}"
188
+ elsif !returns
189
+ options[:returns] = old_returns
190
+ end
191
+ end
192
+ if options.has_key?(:reuse) && old_model.options.has_key?(:reuse) && options[:reuse] != old_model.reuse
193
+ raise ArgumentError, "the reuse flag for #{name}h as already been set to #{options[:reuse]} on our parent model"
194
+ elsif !options.has_key?(:reuse) && old_model.options.has_key?(:reuse)
195
+ options[:reuse] = old_model.reuse
196
+ end
197
+ end
198
+
199
+ # Do not allow changing this model anymore
200
+ def freeze
201
+ options.freeze
202
+ super
203
+ end
204
+
205
+ def initialize_copy(from) # :nodoc:
206
+ @name = from.name.dup
207
+ @options = from.options.dup
208
+ end
209
+
210
+ def to_s; "#{name}(#{options})" end
211
+ end
212
+
213
+ # A planner searches a suitable development for a set of methods.
214
+ # Methods are defined using Planner::method. You can then ask
215
+ # for a plan by sending your method name to the Planner object
216
+ #
217
+ # For instance
218
+ #
219
+ # class MyPlanner < Planner
220
+ # method(:do_it) { }
221
+ # method(:do_sth_else) { ... }
222
+ # end
223
+ #
224
+ # planner = MyPlanner.new
225
+ # planner.do_it => result of the do_it block
226
+ #
227
+ # See Planner::method for a detailed description of the development
228
+ # search
229
+ #
230
+ class Planner
231
+ extend Tools
232
+
233
+ # The resulting plan
234
+ attr_reader :plan
235
+
236
+ # Creates a Planner object which acts on +plan+
237
+ def initialize(plan)
238
+ @plan = plan
239
+ @stack = Array.new
240
+ @arguments = Array.new
241
+ end
242
+
243
+ # A list of options on which the methods are selected
244
+ # in find_methods
245
+ #
246
+ # When calling a planning method, only the methods for
247
+ # which these options match the user-provided options
248
+ # are called. The other options are not considered
249
+ METHOD_SELECTION_OPTIONS = [:id, :recursive, :returns]
250
+ KNOWN_OPTIONS = [:lazy, :reuse, :args] + METHOD_SELECTION_OPTIONS
251
+
252
+ def self.validate_method_query(name, options)
253
+ name = name.to_s
254
+ roby_options, method_arguments =
255
+ filter_options options, KNOWN_OPTIONS
256
+
257
+ validate_option(options, :returns, false,
258
+ "the ':returns' option must be a task model") do |opt|
259
+ opt.is_a?(Roby::TaskModelTag) ||
260
+ opt.has_ancestor?(Roby::Task)
261
+ end
262
+
263
+ [name, roby_options, method_arguments]
264
+ end
265
+
266
+ # Return the method model for +name+, or nil
267
+ def self.method_model(name)
268
+ model = send("#{name}_model")
269
+ rescue NoMethodError
270
+ end
271
+
272
+ class << self
273
+ def last_id; @@last_id ||= 0 end
274
+ def last_id=(new_value); @@last_id = new_value end
275
+ def next_id; self.last_id += 1 end
276
+ end
277
+
278
+ # Some validation on the method IDs
279
+ # * an integer represented as a string is converted to integer form
280
+ # * a symbol is converted to string
281
+ def self.validate_method_id(method_id)
282
+ method_id = method_id.to_s if Symbol === method_id
283
+
284
+ if method_id.respond_to?(:to_str) && method_id.to_str =~ /^\d+$/
285
+ Integer(method_id)
286
+ else
287
+ method_id
288
+ end
289
+ end
290
+
291
+ # Creates, overloads or updates a method model
292
+ # Returns the MethodModel object
293
+ def self.update_method_model(name, options)
294
+ name = name.to_s
295
+ unless send("enum_#{name}_methods", nil).empty?
296
+ raise ArgumentError, "cannot change the method model for #{name} since methods are already using it"
297
+ end
298
+
299
+ old_model = method_model(name)
300
+ new_model = MethodModel.new(name)
301
+ new_model.merge(options)
302
+
303
+ if old_model == new_model
304
+ if !instance_variable_get("@#{name}_model")
305
+ instance_variable_set("@#{name}_model", new_model)
306
+ end
307
+ return new_model
308
+ elsif instance_variable_get("@#{name}_model")
309
+ # old_model is defined at this level
310
+ return old_model.merge(options)
311
+ else
312
+ unless respond_to?("#{name}_model")
313
+ singleton_class.class_eval <<-EOD
314
+ def #{name}_model
315
+ @#{name}_model || superclass.#{name}_model
316
+ end
317
+ EOD
318
+ end
319
+ new_model.overload(old_model) if old_model
320
+ instance_variable_set("@#{name}_model", new_model)
321
+ end
322
+ end
323
+
324
+ # call-seq:
325
+ # method(name, option1 => value1, option2 => value2) { } => method definition
326
+ # method(name, option1 => value1, option2 => value2) => method model
327
+ #
328
+ # In the first form, define a new method +name+. The given block
329
+ # is used as method definition. It shall either return a Task
330
+ # object or an object whose #each method yields task objects, or
331
+ # raise a PlanModelError exception, or of one of its subclasses.
332
+ #
333
+ # The second form defines a method model, which defines
334
+ # constraints on the method defined with this name
335
+ #
336
+ # == Overloading: using the +id+ option
337
+ # The +id+ option defines the method ID. The ID can be used
338
+ # to override a method definition in submodels. For instance,
339
+ # if you do
340
+ #
341
+ # class A < Planner
342
+ # method(:do_it, :id => 'first') { ... }
343
+ # method(:do_it, :id => 'second') { ... }
344
+ # end
345
+ # class B < A
346
+ # method(:do_it, :id => 'first') { ... }
347
+ # end
348
+ #
349
+ # Then calling B.new.do_it will call the +first+ method defined
350
+ # in B and the +second+ defined in A
351
+ #
352
+ # If no method ID is given, an unique number is allocated. Try not
353
+ # using numbers as method IDs yourself, since you could overload
354
+ # an automatic ID.
355
+ #
356
+ # == Constraining the returned object
357
+ # The +returns+ option defines what kind of Task object this method
358
+ # shall return.
359
+ #
360
+ # For instance, in
361
+ #
362
+ # class A < Planner
363
+ # method(:do_it, :id => 'first', :returns => MyTask) { ... }
364
+ # method(:do_it, :id => 'second') { ... }
365
+ # end
366
+ # class B < A
367
+ # method(:do_it, :id => 'first') { ... }
368
+ # end
369
+ #
370
+ # The +do_it+ method defined in B will have to return a MyTask-derived
371
+ # Task object. Method models can be used to put a constraint on all
372
+ # methods of a given name. For instance, in the following example, all
373
+ # +do_it+ methods would have to return MyTask-based objects
374
+ #
375
+ # class A < Planner
376
+ # method(:do_it, :returns => MyTask)
377
+ # method(:do_it, :id => 'first') { ... }
378
+ # method(:do_it, :id => 'second') { ... }
379
+ # end
380
+ #
381
+ # == Recursive call to methods
382
+ # If the +recursive+ option is true, then the method can be called back even if it
383
+ # currently being developed. The default is false
384
+ #
385
+ # For instance, the following example will raise a NoMethodError:
386
+ #
387
+ # class A < Planner
388
+ # method(:do_it) { do_it }
389
+ # end
390
+ # A.new.do_it
391
+ #
392
+ # while this one will behave properly
393
+ #
394
+ # class A < Planner
395
+ # method(:do_it) { do_it }
396
+ # method(:do_it, :recursive => true) { ... }
397
+ # end
398
+ #
399
+ # == Reusing already existing tasks in plan
400
+ # If the +reuse+ flag is set (the default), instead of calling a method
401
+ # definition, the planner will try to find a suitable task in the current
402
+ # plan if the developed method defines a :returns attribute. Compatibility
403
+ # is checked using Task#fullfills?
404
+ #
405
+ # == Defined attributes
406
+ # For each method +name+, the planner class gets a few attributes and methods:
407
+ # * each_name_method iterates on all MethodDefinition objects for +name+
408
+ # * name_model returns the method model. It is not defined if no method model exists
409
+ # * each_name_filter iterates on all filters for +name+
410
+ def self.method(name, options = Hash.new, &body)
411
+ name, options = validate_method_query(name, options)
412
+
413
+ # Define the method enumerator and the method selection
414
+ if !respond_to?("#{name}_methods")
415
+ inherited_enumerable("#{name}_method", "#{name}_methods", :map => true) { Hash.new }
416
+ class_eval <<-PLANNING_METHOD_END
417
+ def #{name}(options = Hash.new)
418
+ plan_method("#{name}", options)
419
+ end
420
+ class << self
421
+ cached_enum("#{name}_method", "#{name}_methods", true)
422
+ end
423
+ PLANNING_METHOD_END
424
+ end
425
+
426
+ # We are updating the method model
427
+ if !body
428
+ return update_method_model(name, options)
429
+ end
430
+
431
+ # Handle the method ID
432
+ if method_id = options[:id]
433
+ method_id = validate_method_id(method_id)
434
+ if method_id.respond_to?(:to_int)
435
+ self.last_id = method_id if self.last_id < method_id
436
+ end
437
+ else
438
+ method_id = next_id
439
+ end
440
+ options[:id] = method_id
441
+
442
+ # Get the method model (if any)
443
+ if model = method_model(name)
444
+ options = model.validate(options)
445
+ model.freeze
446
+ end
447
+
448
+ # Check if we are overloading an old method
449
+ if send("#{name}_methods")[method_id]
450
+ raise ArgumentError, "method #{name}:#{method_id} is already defined on this planning model"
451
+ elsif old_method = find_methods(name, :id => method_id)
452
+ old_method = *old_method
453
+ options = old_method.validate(options)
454
+ Planning.debug { "overloading #{name}:#{method_id}" }
455
+ end
456
+
457
+ # Register the method definition
458
+ #
459
+ # First, define an "anonymous" method on this planner model to
460
+ # avoid calling instance_eval during planning
461
+ if body.arity > 0
462
+ raise ArgumentError, "method body must accept zero arguments calls"
463
+ end
464
+ temp_method_name = "m#{@@temp_method_id += 1}"
465
+ define_method(temp_method_name, &body)
466
+ send("#{name}_methods")[method_id] = MethodDefinition.new(name, options, instance_method(temp_method_name))
467
+ end
468
+ @@temp_method_id = 0
469
+
470
+ # Returns an array of the names of all planning methods
471
+ def self.planning_methods_names
472
+ names = Set.new
473
+ methods.each do |method_name|
474
+ if method_name =~ /^each_(\w+)_method$/
475
+ names << $1
476
+ end
477
+ end
478
+
479
+ names
480
+ end
481
+
482
+ def self.clear_model
483
+ planning_methods_names.each do |name|
484
+ remove_planning_method(name)
485
+ end
486
+ end
487
+
488
+ # Undefines all the definitions for the planning method +name+ on
489
+ # this model. Definitions available on the parent are not removed
490
+ def self.remove_planning_method(name)
491
+ remove_method(name)
492
+ remove_inherited_enumerable("#{name}_method", "#{name}_methods")
493
+ if method_defined?("#{name}_filter")
494
+ remove_inherited_enumerable("#{name}_filter", "#{name}_filters")
495
+ end
496
+ end
497
+
498
+ def self.remove_inherited_enumerable(enum, attr = enum)
499
+ if instance_variable_defined?("@#{attr}")
500
+ remove_instance_variable("@#{attr}")
501
+ end
502
+ singleton_class.class_eval do
503
+ remove_method("each_#{enum}")
504
+ remove_method(attr)
505
+ end
506
+ end
507
+
508
+ # Add a selection filter on the +name+ method. When developing the
509
+ # +name+ method, the filter is called with the method options and
510
+ # the MethodDefinition object, and should return +false+ if the
511
+ # method is to be discarded, and +true+ otherwise
512
+ #
513
+ # Example
514
+ # class MyPlanner < Planning::Planner
515
+ # method(:m, :id => 1) do
516
+ # raise
517
+ # end
518
+ #
519
+ # method(:m, :id => 2) do
520
+ # Roby::Task.new
521
+ # end
522
+ #
523
+ # # the id == 1 version of m fails, remove it of the set
524
+ # # of valid methods
525
+ # filter(:m) do |opts, m|
526
+ # m.id == 2
527
+ # end
528
+ # end
529
+ #
530
+ # This is mainly useful for external selection of methods (for
531
+ # instance to implement some kind of dependency injection), or for
532
+ # testing
533
+ def self.filter(name, &filter)
534
+ check_arity(filter, 2)
535
+
536
+ if !respond_to?("#{name}_filters")
537
+ inherited_enumerable("#{name}_filter", "#{name}_filters") { Array.new }
538
+ class_eval <<-EOD
539
+ class << self
540
+ cached_enum("#{name}_filter", "#{name}_filters", false)
541
+ end
542
+ EOD
543
+ end
544
+ send("#{name}_filters") << filter
545
+ end
546
+
547
+ def self.each_method(name, id, &iterator)
548
+ send("each_#{name}_method", id, &iterator)
549
+ end
550
+
551
+ # Find all methods that can be used to plan +[name, options]+. The selection is
552
+ # done in two steps:
553
+ # * we search all definition of +name+ that are compatible with +options. In this
554
+ # stage, only the options listed in METHOD_SELECTION_OPTIONS are compared
555
+ # * we call the method filters (if any) to remove unsuitable methods
556
+ def self.find_methods(name, options = Hash.new)
557
+ # validate the options hash, and split it into the options that are used for
558
+ # method selection and the ones that are ignored here
559
+ name, options = validate_method_query(name, options)
560
+ method_selection = options.slice(*METHOD_SELECTION_OPTIONS)
561
+
562
+ if method_id = method_selection[:id]
563
+ method_selection[:id] = method_id = validate_method_id(method_id)
564
+ result = send("enum_#{name}_methods", method_id).find { true }
565
+ result = if result && result.options.merge(method_selection) == result.options
566
+ [result]
567
+ end
568
+ else
569
+ result = send("enum_#{name}_methods", nil).collect do |id, m|
570
+ if m.options.merge(method_selection) == m.options
571
+ m
572
+ end
573
+ end.compact
574
+ end
575
+
576
+ return nil if !result
577
+
578
+ filter_method = "enum_#{name}_filters"
579
+ if respond_to?(filter_method)
580
+ # Remove results for which at least one filter returns false
581
+ result.reject! { |m| send(filter_method).any? { |f| !f[options, m] } }
582
+ end
583
+
584
+ if result.empty?; nil
585
+ else; result
586
+ end
587
+ end
588
+
589
+ # If there is method definitions for +name+
590
+ def has_method?(name); singleton_class.has_method?(name) end
591
+ def self.has_method?(name); respond_to?("#{name}_methods") end
592
+
593
+ # Returns the method model that should be considered when using
594
+ # the result of the method +name+ with options +options+
595
+ #
596
+ # This model should be used for instance when adding a new
597
+ # hierarchy relation between a parent and the result of
598
+ # <tt>plan.#{name}(options)</tt>
599
+ def self.model_of(name, options = {})
600
+ model = if options[:id]
601
+ enum_for("each_method", name, options[:id]).find { true }
602
+ end
603
+ model ||= method_model(name)
604
+ model || default_method_model(name)
605
+ end
606
+
607
+ def self.default_method_model(name)
608
+ MethodModel.new(name, :returns => Task)
609
+ end
610
+
611
+ # Creates a planning task which will call the same planning method
612
+ # than the one currently being generated.
613
+ #
614
+ # +options+ is an option hash. These options are used to override
615
+ # the current method options. Only one option is recognized by
616
+ # +replan_task+:
617
+ #
618
+ # strict:: if true, we use the current method name and id for
619
+ # the planning task. If false, use only the method name.
620
+ # defaults to true.
621
+ def replan_task(options = nil)
622
+ method_options = arguments.dup
623
+ if !options.has_key?(:strict) || options.delete(:strict)
624
+ method_options.merge!(:id => @stack.last[1])
625
+ end
626
+
627
+ if options
628
+ method_options.merge!(options)
629
+ end
630
+
631
+ Roby::PlanningTask.new :planner_model => self.class,
632
+ :method_name => @stack.last[0],
633
+ :method_options => method_options
634
+ end
635
+
636
+ def stop; @stop_required = true end
637
+ def interruption_point; raise Interrupt, "interrupted planner" if @stop_required end
638
+
639
+ # Find a suitable development for the +name+ method.
640
+ def plan_method(name, options = Hash.new)
641
+ if @stack.empty?
642
+ @stop_required = false
643
+ end
644
+ interruption_point
645
+
646
+ name = name.to_s
647
+
648
+ planning_options, method_options =
649
+ filter_options options, KNOWN_OPTIONS
650
+
651
+ if method_options.empty?
652
+ method_options = planning_options.delete(:args) || {}
653
+ elsif planning_options[:args] && !planning_options[:args].empty?
654
+ raise ArgumentError, "provided method-specific options through both :args and the option hash"
655
+ end
656
+ @arguments.push(method_options)
657
+
658
+ Planning.debug { "planning #{name}[#{arguments}]" }
659
+
660
+ # Check for recursion
661
+ if (options[:id] && @stack.include?([name, options[:id]])) || (!options[:id] && @stack.find { |n, _| n == name })
662
+ options[:recursive] = true
663
+ end
664
+
665
+ # Get all valid methods. If no candidate are found, still try
666
+ # to get a task to re-use
667
+ methods = singleton_class.find_methods(name, options)
668
+
669
+ # Check if we can reuse a task already in the plan
670
+ if !options.has_key?(:reuse) || options[:reuse]
671
+ all_returns = if methods
672
+ methods.map { |m| m.returns if m.reuse? }
673
+ else []
674
+ end
675
+ if (model = singleton_class.method_model(name)) && !options[:id]
676
+ all_returns << model.returns if model.reuse?
677
+ end
678
+ all_returns.compact!
679
+
680
+ for return_type in all_returns
681
+ if task = find_reusable_task(return_type)
682
+ return task
683
+ end
684
+ end
685
+ end
686
+
687
+ if !methods || methods.empty?
688
+ raise NotFound.new(self, Hash.new)
689
+ end
690
+
691
+ # Call the methods
692
+ call_planning_methods(Hash.new, options, *methods)
693
+
694
+ rescue Interrupt
695
+ raise
696
+
697
+ rescue NotFound => e
698
+ e.method_name = name
699
+ e.method_options = options
700
+ raise e
701
+
702
+ ensure
703
+ @arguments.pop
704
+ end
705
+
706
+ def find_reusable_task(return_type)
707
+ query = plan.find_tasks.
708
+ which_fullfills(return_type, arguments).
709
+ self_owned.
710
+ not_abstract.
711
+ not_finished.
712
+ roots(TaskStructure::Hierarchy)
713
+
714
+ for candidate in query
715
+ Planning.debug { "selecting task #{candidate} instead of planning #{return_type}[#{arguments}]" }
716
+ return candidate
717
+ end
718
+ nil
719
+ end
720
+
721
+ def arguments; @arguments.last end
722
+ private :arguments
723
+
724
+ # Tries to find a successfull development in the provided method list.
725
+ #
726
+ # It raises NotFound if none of the methods returned successfully
727
+ def call_planning_methods(errors, options, method, *methods)
728
+ begin
729
+ @stack.push [method.name, method.id]
730
+ Planning.debug { "calling #{method.name}:#{method.id} with arguments #{arguments}" }
731
+ begin
732
+ result = method.call(self)
733
+ rescue PlanModelError, Interrupt
734
+ raise
735
+ rescue Exception => e
736
+ raise PlanModelError.new(self), e.message, e.backtrace
737
+ end
738
+
739
+ # Check that result is a task or a task collection
740
+ unless result && (result.respond_to?(:to_task) || result.respond_to?(:each) || !result.respond_to?(:each_task))
741
+ raise PlanModelError.new(self), "#{method} returned #{result}, which is neither a task nor a task collection"
742
+ end
743
+
744
+ # Insert resulting tasks in +plan+
745
+ plan.discover(result)
746
+
747
+ expected_return = method.returns
748
+ if expected_return
749
+ if !result.respond_to?(:to_task) ||
750
+ !result.fullfills?(expected_return, arguments.slice(*expected_return.arguments))
751
+
752
+ if !result then result = "nil"
753
+ elsif result.respond_to?(:each)
754
+ result = result.map { |t| "#{t}(#{t.arguments})" }.join(", ")
755
+ else result = "#{result}(#{result.arguments})"
756
+ end
757
+ raise PlanModelError.new(self), "#{method} returned #{result} which does not fullfill #{method.returns}(#{arguments})"
758
+ end
759
+ end
760
+ Planning.debug { "found #{result}" }
761
+
762
+ result
763
+
764
+ ensure
765
+ @stack.pop
766
+ end
767
+
768
+ rescue PlanModelError => e
769
+ e.planner = self unless e.planner
770
+ errors[method] = e
771
+ if methods.empty?
772
+ raise NotFound.new(self, errors)
773
+ else
774
+ call_planning_methods(errors, options, *methods)
775
+ end
776
+ end
777
+
778
+ private :call_planning_methods
779
+
780
+ # Builds a loop in a plan (i.e. a method which is generated in
781
+ # loop)
782
+ def make_loop(options = {}, &block)
783
+ raise ArgumentError, "no block given" unless block
784
+
785
+ options.merge! :planner_model => self.class, :method_name => 'loops'
786
+ _, planning_options = PlanningLoop.filter_options(options)
787
+
788
+ loop_id = Planner.next_id
789
+ if !@stack.empty?
790
+ loop_id = "#{@stack.last[1]}_#{loop_id}"
791
+ end
792
+ planning_options[:id] = loop_id
793
+ planning_options[:reuse] = false
794
+ m = self.class.method('loops', planning_options, &block)
795
+
796
+ options[:method_options] ||= {}
797
+ options[:method_options].merge!(arguments || {})
798
+ options[:method_options][:id] = m.id
799
+ PlanningLoop.new(options)
800
+ end
801
+ end
802
+
803
+ # A planning Library is only a way to gather a set of planning
804
+ # methods. It is created by
805
+ # module MyLibrary
806
+ # planning_library
807
+ # method(:bla) do
808
+ # end
809
+ # end
810
+ # or
811
+ # my_library = Roby::Planning::Library.new do
812
+ # method(:bla) do end
813
+ # end
814
+ #
815
+ # It is then used by simply including the library in another library
816
+ # or in a Planner class
817
+ #
818
+ # module AnotherLibrary
819
+ # include MyLibrary
820
+ # end
821
+ #
822
+ # class MyPlanner < Planner
823
+ # include AnotherLibrary
824
+ # end
825
+ #
826
+ # Alternatively, you can use Planner::use and Library::use, which search
827
+ # for a Planning module in the given module. For instance
828
+ #
829
+ # module Namespace
830
+ # module Planning
831
+ # planning_library
832
+ # [...]
833
+ # end
834
+ # end
835
+ #
836
+ # can be used with
837
+ #
838
+ # class MyPlanner < Planner
839
+ # using Namespace
840
+ # end
841
+ #
842
+ module Library
843
+ include Tools
844
+
845
+ attr_reader :default_options
846
+
847
+ def planning_methods; @methods ||= Array.new end
848
+ def method(name, options = Hash.new, &body)
849
+ if body && default_options
850
+ options = default_options.merge(options)
851
+ end
852
+ planning_methods << [name, options, body]
853
+ end
854
+
855
+ def self.clear_model
856
+ planning_methods.clear
857
+ end
858
+
859
+ # Cannot use included here because included() is called *after* the module
860
+ # has been included
861
+ def append_features(klass)
862
+ new_libraries = ancestors.enum_for.
863
+ reject { |mod| klass < mod }.
864
+ find_all { |mod| mod.respond_to?(:planning_methods) }
865
+
866
+ super
867
+
868
+ unless klass < Planner
869
+ if Class === klass
870
+ Roby.debug "including a planning library in a class which is not a Planner, which is useless"
871
+ else
872
+ klass.extend Library
873
+ end
874
+ return
875
+ end
876
+
877
+ new_libraries.reverse_each do |mod|
878
+ mod.planning_methods.each do |name, options, body|
879
+ begin
880
+ klass.method(name, options, &body)
881
+ rescue ArgumentError => e
882
+ raise ArgumentError, "cannot include the #{self} library in #{klass}: when inserting #{name}#{options}, #{e.message}", caller(0)
883
+ end
884
+ end
885
+ end
886
+ end
887
+
888
+ def self.new(&block)
889
+ Module.new do
890
+ extend Library
891
+ class_eval(&block)
892
+ end
893
+ end
894
+ end
895
+
896
+ end
897
+ end
898
+
899
+
900
+ class Module
901
+ def planning_library(default_options = Hash.new)
902
+ extend Roby::Planning::Library
903
+ instance_variable_set(:@default_options, default_options)
904
+ end
905
+ end
906
+