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.
- data/.gitignore +29 -0
- data/History.txt +4 -0
- data/License-fr.txt +519 -0
- data/License.txt +515 -0
- data/Manifest.txt +245 -0
- data/NOTES +4 -0
- data/README.txt +163 -0
- data/Rakefile +161 -0
- data/TODO.txt +146 -0
- data/app/README.txt +24 -0
- data/app/Rakefile +8 -0
- data/app/config/ROBOT.rb +5 -0
- data/app/config/app.yml +91 -0
- data/app/config/init.rb +7 -0
- data/app/config/roby.yml +3 -0
- data/app/controllers/.gitattributes +0 -0
- data/app/controllers/ROBOT.rb +2 -0
- data/app/data/.gitattributes +0 -0
- data/app/planners/ROBOT/main.rb +6 -0
- data/app/planners/main.rb +5 -0
- data/app/scripts/distributed +3 -0
- data/app/scripts/generate/bookmarks +3 -0
- data/app/scripts/replay +3 -0
- data/app/scripts/results +3 -0
- data/app/scripts/run +3 -0
- data/app/scripts/server +3 -0
- data/app/scripts/shell +3 -0
- data/app/scripts/test +3 -0
- data/app/tasks/.gitattributes +0 -0
- data/app/tasks/ROBOT/.gitattributes +0 -0
- data/bin/roby +210 -0
- data/bin/roby-log +168 -0
- data/bin/roby-shell +25 -0
- data/doc/images/event_generalization.png +0 -0
- data/doc/images/exception_propagation_1.png +0 -0
- data/doc/images/exception_propagation_2.png +0 -0
- data/doc/images/exception_propagation_3.png +0 -0
- data/doc/images/exception_propagation_4.png +0 -0
- data/doc/images/exception_propagation_5.png +0 -0
- data/doc/images/replay_handler_error.png +0 -0
- data/doc/images/replay_handler_error_0.png +0 -0
- data/doc/images/replay_handler_error_1.png +0 -0
- data/doc/images/roby_cycle_overview.png +0 -0
- data/doc/images/roby_replay_02.png +0 -0
- data/doc/images/roby_replay_03.png +0 -0
- data/doc/images/roby_replay_04.png +0 -0
- data/doc/images/roby_replay_event_representation.png +0 -0
- data/doc/images/roby_replay_first_state.png +0 -0
- data/doc/images/roby_replay_relations.png +0 -0
- data/doc/images/roby_replay_startup.png +0 -0
- data/doc/images/task_event_generalization.png +0 -0
- data/doc/papers.rdoc +11 -0
- data/doc/styles/allison.css +314 -0
- data/doc/styles/allison.js +316 -0
- data/doc/styles/allison.rb +276 -0
- data/doc/styles/jamis.rb +593 -0
- data/doc/tutorials/01-GettingStarted.rdoc +86 -0
- data/doc/tutorials/02-GoForward.rdoc +220 -0
- data/doc/tutorials/03-PlannedPath.rdoc +268 -0
- data/doc/tutorials/04-EventPropagation.rdoc +236 -0
- data/doc/tutorials/05-ErrorHandling.rdoc +319 -0
- data/doc/tutorials/06-Overview.rdoc +40 -0
- data/doc/videos.rdoc +69 -0
- data/ext/droby/dump.cc +175 -0
- data/ext/droby/extconf.rb +3 -0
- data/ext/graph/algorithm.cc +746 -0
- data/ext/graph/extconf.rb +7 -0
- data/ext/graph/graph.cc +529 -0
- data/ext/graph/graph.hh +183 -0
- data/ext/graph/iterator_sequence.hh +102 -0
- data/ext/graph/undirected_dfs.hh +226 -0
- data/ext/graph/undirected_graph.hh +421 -0
- data/lib/roby.rb +41 -0
- data/lib/roby/app.rb +870 -0
- data/lib/roby/app/rake.rb +56 -0
- data/lib/roby/app/run.rb +14 -0
- data/lib/roby/app/scripts/distributed.rb +13 -0
- data/lib/roby/app/scripts/generate/bookmarks.rb +162 -0
- data/lib/roby/app/scripts/replay.rb +31 -0
- data/lib/roby/app/scripts/results.rb +15 -0
- data/lib/roby/app/scripts/run.rb +26 -0
- data/lib/roby/app/scripts/server.rb +18 -0
- data/lib/roby/app/scripts/shell.rb +88 -0
- data/lib/roby/app/scripts/test.rb +40 -0
- data/lib/roby/basic_object.rb +151 -0
- data/lib/roby/config.rb +5 -0
- data/lib/roby/control.rb +747 -0
- data/lib/roby/decision_control.rb +17 -0
- data/lib/roby/distributed.rb +32 -0
- data/lib/roby/distributed/base.rb +440 -0
- data/lib/roby/distributed/communication.rb +871 -0
- data/lib/roby/distributed/connection_space.rb +592 -0
- data/lib/roby/distributed/distributed_object.rb +206 -0
- data/lib/roby/distributed/drb.rb +62 -0
- data/lib/roby/distributed/notifications.rb +539 -0
- data/lib/roby/distributed/peer.rb +550 -0
- data/lib/roby/distributed/protocol.rb +529 -0
- data/lib/roby/distributed/proxy.rb +343 -0
- data/lib/roby/distributed/subscription.rb +311 -0
- data/lib/roby/distributed/transaction.rb +498 -0
- data/lib/roby/event.rb +897 -0
- data/lib/roby/exceptions.rb +234 -0
- data/lib/roby/executives/simple.rb +30 -0
- data/lib/roby/graph.rb +166 -0
- data/lib/roby/interface.rb +390 -0
- data/lib/roby/log.rb +3 -0
- data/lib/roby/log/chronicle.rb +303 -0
- data/lib/roby/log/console.rb +72 -0
- data/lib/roby/log/data_stream.rb +197 -0
- data/lib/roby/log/dot.rb +279 -0
- data/lib/roby/log/event_stream.rb +151 -0
- data/lib/roby/log/file.rb +340 -0
- data/lib/roby/log/gui/basic_display.ui +83 -0
- data/lib/roby/log/gui/chronicle.rb +26 -0
- data/lib/roby/log/gui/chronicle_view.rb +40 -0
- data/lib/roby/log/gui/chronicle_view.ui +70 -0
- data/lib/roby/log/gui/data_displays.rb +172 -0
- data/lib/roby/log/gui/data_displays.ui +155 -0
- data/lib/roby/log/gui/notifications.rb +26 -0
- data/lib/roby/log/gui/relations.rb +248 -0
- data/lib/roby/log/gui/relations.ui +123 -0
- data/lib/roby/log/gui/relations_view.rb +185 -0
- data/lib/roby/log/gui/relations_view.ui +149 -0
- data/lib/roby/log/gui/replay.rb +327 -0
- data/lib/roby/log/gui/replay_controls.rb +200 -0
- data/lib/roby/log/gui/replay_controls.ui +259 -0
- data/lib/roby/log/gui/runtime.rb +130 -0
- data/lib/roby/log/hooks.rb +185 -0
- data/lib/roby/log/logger.rb +202 -0
- data/lib/roby/log/notifications.rb +244 -0
- data/lib/roby/log/plan_rebuilder.rb +470 -0
- data/lib/roby/log/relations.rb +1056 -0
- data/lib/roby/log/server.rb +550 -0
- data/lib/roby/log/sqlite.rb +47 -0
- data/lib/roby/log/timings.rb +164 -0
- data/lib/roby/plan-object.rb +247 -0
- data/lib/roby/plan.rb +762 -0
- data/lib/roby/planning.rb +13 -0
- data/lib/roby/planning/loops.rb +302 -0
- data/lib/roby/planning/model.rb +906 -0
- data/lib/roby/planning/task.rb +151 -0
- data/lib/roby/propagation.rb +562 -0
- data/lib/roby/query.rb +619 -0
- data/lib/roby/relations.rb +583 -0
- data/lib/roby/relations/conflicts.rb +70 -0
- data/lib/roby/relations/ensured.rb +20 -0
- data/lib/roby/relations/error_handling.rb +23 -0
- data/lib/roby/relations/events.rb +9 -0
- data/lib/roby/relations/executed_by.rb +193 -0
- data/lib/roby/relations/hierarchy.rb +239 -0
- data/lib/roby/relations/influence.rb +10 -0
- data/lib/roby/relations/planned_by.rb +63 -0
- data/lib/roby/robot.rb +7 -0
- data/lib/roby/standard_errors.rb +218 -0
- data/lib/roby/state.rb +5 -0
- data/lib/roby/state/events.rb +221 -0
- data/lib/roby/state/information.rb +55 -0
- data/lib/roby/state/pos.rb +110 -0
- data/lib/roby/state/shapes.rb +32 -0
- data/lib/roby/state/state.rb +353 -0
- data/lib/roby/support.rb +92 -0
- data/lib/roby/task-operations.rb +182 -0
- data/lib/roby/task.rb +1618 -0
- data/lib/roby/test/common.rb +399 -0
- data/lib/roby/test/distributed.rb +214 -0
- data/lib/roby/test/tasks/empty_task.rb +9 -0
- data/lib/roby/test/tasks/goto.rb +36 -0
- data/lib/roby/test/tasks/simple_task.rb +23 -0
- data/lib/roby/test/testcase.rb +519 -0
- data/lib/roby/test/tools.rb +160 -0
- data/lib/roby/thread_task.rb +87 -0
- data/lib/roby/transactions.rb +462 -0
- data/lib/roby/transactions/proxy.rb +292 -0
- data/lib/roby/transactions/updates.rb +139 -0
- data/plugins/fault_injection/History.txt +4 -0
- data/plugins/fault_injection/README.txt +37 -0
- data/plugins/fault_injection/Rakefile +18 -0
- data/plugins/fault_injection/TODO.txt +0 -0
- data/plugins/fault_injection/app.rb +52 -0
- data/plugins/fault_injection/fault_injection.rb +89 -0
- data/plugins/fault_injection/test/test_fault_injection.rb +84 -0
- data/plugins/subsystems/README.txt +40 -0
- data/plugins/subsystems/Rakefile +18 -0
- data/plugins/subsystems/app.rb +171 -0
- data/plugins/subsystems/test/app/README +24 -0
- data/plugins/subsystems/test/app/Rakefile +8 -0
- data/plugins/subsystems/test/app/config/app.yml +71 -0
- data/plugins/subsystems/test/app/config/init.rb +9 -0
- data/plugins/subsystems/test/app/config/roby.yml +3 -0
- data/plugins/subsystems/test/app/planners/main.rb +20 -0
- data/plugins/subsystems/test/app/scripts/distributed +3 -0
- data/plugins/subsystems/test/app/scripts/replay +3 -0
- data/plugins/subsystems/test/app/scripts/results +3 -0
- data/plugins/subsystems/test/app/scripts/run +3 -0
- data/plugins/subsystems/test/app/scripts/server +3 -0
- data/plugins/subsystems/test/app/scripts/shell +3 -0
- data/plugins/subsystems/test/app/scripts/test +3 -0
- data/plugins/subsystems/test/app/tasks/services.rb +15 -0
- data/plugins/subsystems/test/test_subsystems.rb +71 -0
- data/test/distributed/test_communication.rb +178 -0
- data/test/distributed/test_connection.rb +282 -0
- data/test/distributed/test_execution.rb +373 -0
- data/test/distributed/test_mixed_plan.rb +341 -0
- data/test/distributed/test_plan_notifications.rb +238 -0
- data/test/distributed/test_protocol.rb +516 -0
- data/test/distributed/test_query.rb +102 -0
- data/test/distributed/test_remote_plan.rb +491 -0
- data/test/distributed/test_transaction.rb +463 -0
- data/test/mockups/tasks.rb +27 -0
- data/test/planning/test_loops.rb +380 -0
- data/test/planning/test_model.rb +427 -0
- data/test/planning/test_task.rb +106 -0
- data/test/relations/test_conflicts.rb +42 -0
- data/test/relations/test_ensured.rb +38 -0
- data/test/relations/test_executed_by.rb +149 -0
- data/test/relations/test_hierarchy.rb +158 -0
- data/test/relations/test_planned_by.rb +54 -0
- data/test/suite_core.rb +24 -0
- data/test/suite_distributed.rb +9 -0
- data/test/suite_planning.rb +3 -0
- data/test/suite_relations.rb +8 -0
- data/test/test_bgl.rb +508 -0
- data/test/test_control.rb +399 -0
- data/test/test_event.rb +894 -0
- data/test/test_exceptions.rb +592 -0
- data/test/test_interface.rb +37 -0
- data/test/test_log.rb +114 -0
- data/test/test_log_server.rb +132 -0
- data/test/test_plan.rb +584 -0
- data/test/test_propagation.rb +210 -0
- data/test/test_query.rb +266 -0
- data/test/test_relations.rb +180 -0
- data/test/test_state.rb +414 -0
- data/test/test_support.rb +16 -0
- data/test/test_task.rb +938 -0
- data/test/test_testcase.rb +122 -0
- data/test/test_thread_task.rb +73 -0
- data/test/test_transactions.rb +569 -0
- data/test/test_transactions_proxy.rb +198 -0
- metadata +570 -0
|
@@ -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
|
+
|