{Previous tutorial}[link:files/doc/tutorials/02-GoForward_rdoc.html]
{Next tutorial}[link:files/doc/tutorials/04-EventPropagation_rdoc.html]
= Planning and following a path
We'll now use a (slightly) more complex system to make our robot move. The
robot will now have a goal, defined as a (x, y) point. It will generate a
trajectory which leads it to that goal, and then execute that trajectory.
This tutorial therefore shows the following:
* how multiple activities can be _temporally_ coordinated to make the robot
reach a defined goal, and
* how the plan represents how one activity relates to another.
In this new robot, three activities will be used to make the robot reach
its goal. The plan will therefore represent various things:
* the three activities: the high-level activity which represent the goal of
the robot; the path planning activity and the path execution activity.
* how these activities relate to each other. For that, Roby defines
task relations.
* how the plan describes the temporal relations between these activities (i.e.
when a given activity should be started).
To hold all these, we will create a new robot:
roby robot PathPlan
== Defining the task models
This section will describe the task models, without the actual implementation
of the actual implementation of these activities. That implementation is
discussed later in that tutorial. The goal is to first make you grasp what the
task models, and the plan model is about and only then how the tasks can
actually control the robot itself.
* the +MoveTo+ task express the current goal of the robot, and holds the path
data. Open tasks/move_to.rb and add the following:
class MoveTo < Roby::Task
terminates
# The movement goal
argument :goal
# The generated path
def path; data end
end
* the +ComputePath+ task generates the path on behalf of a +MoveTo+. When
successful, it updates the +data+ attribute of the +MoveTo+ task it is
planning. It uses a standard task, Roby::ThreadTask, which allows to
represent the execution of a separate thread into the main plan. Open
tasks/compute_path.rb and add the following:
require 'roby/thread_task'
class ComputePath < Roby::ThreadTask
# The movement goal
argument :goal
# The maximum speed limit
argument :max_speed
end
* finally, +TrackPath+ takes the path generated and follows it. Open
tasks/track_path.rb and add the following:
class TrackPath < Roby::Task
terminates
# The task holding the path data
argument :path_task
end
*Note*: the file names are "best practice" recommandations. They are not at all
required for the application to work.
== Building the movement plan
Let's add a +move_to+ action to our robot, which builds the plan corresponding
to the whole movement. The action definition, in
planners/PathPlan/main.rb would look like this:
# Note: the method arguments are accessed through the +arguments+ hash
method(:move_to) do
# The goal point
goal = Pos::Vector3D.new(*arguments.values_at(:x, :y))
# The high-level representation of the movement
move = MoveTo.new :goal => goal
move.realized_by compute = ComputePath.new(:goal => goal, :max_speed => 1.0)
move.realized_by track = TrackPath.new(:path_task => move)
move.on :start, compute, :start
compute.on :success, track, :start
track.forward :success, move, :success
move
end
The first part creates the task structure, which expresses the
relationships of the different tasks of the plan. This simple plan uses only
one kind of relation, the RealizedBy relation. In this relation, the child task
(i.e. +compute+ and +track+) are simple activities which achieve the parent's higher-level
action.
The second part creates the event structure, which expresses how the
plan should respond to new situations. In our case, the three line describe the following:
* the path planning must be started when the movement is started. This uses the Signal
event relation.
* the path execution must be started when the path planning has successfully finished, and
* the movement has finished when the path tracking has finished. This uses
the Forward relation.
The difference between those two relations is subtle, so let's try to explain a bit more:
* in the first two cases, what the system must do is executing a new action in
response to a new situation. When the +start+ event of +move+ is emitted, the +move+
activity has just started (i.e. all necessary actions have been taken to start that
new activity). The system should then make what is necessary to start computing the
path: it calls the _command_ of the +start+ event of +compute+.
* in the third case, however, no specific action should be taken to end the
+move+ task. Instead, the plan expresses that the +move+ task is finished
as soon as the +track+ task is. Another way to put it is that the
situation represented by the +success+ event of MoveTo is, in this particular
plan, the same than the situation represented by the +success+ event of
TrackPath. More generally, if +a+ is forwarded to +b+ all situations that lead
to the emission of +a+ also lead to the emission of +b+. Or, in other words, that
the situation represented by +b+ is a superset of the one represented by +a+
(the equality, like here, is a particular case)
link:../../images/event_generalization.png
*Example*: in this plan, the +success+ event of a particular low-level action
is forwarded in more high-level parts of the plan. This allows to actually
link the low and high level parts of the plan and reason on that link.
Task relations allow the system to keep track of what a given task is useful for,
what are error conditions and how to react to errors. The next two tutorials will
describe these parts in more details.
== Running this unfinished controller
Let's run this controller. Launch the controller
$ scripts/run PathPlan
In another terminal, launch the shell and start the move_to! action
$ scripts/shell
>> move_to! :x => 10, :y => 10
=> MoveTo{goal => Vector3D(x=10.000000,y=10.000000,z=0.000000)}:0x48350370[]
>>
!Roby::ChildFailedError
!at [336040:01:45.419/186] in the failed event of ComputePath:0x483502e0
!block not supplied (ArgumentError)
! /home/doudou/dev/roby/lib/roby/thread_task.rb:51:in `instance_eval',
! /home/doudou/dev/roby/lib/roby/thread_task.rb:61:in `value',
! /home/doudou/dev/roby/lib/roby/thread_task.rb:61:in the polling handler,
! /home/doudou/system/powerpc-linux/ruby-1.8.6/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require',
! /home/doudou/system/powerpc-linux/ruby-1.8.6/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require',
! scripts/run:3
!
!The failed relation is
! MoveTo:0x48350370
! owners: Roby::Distributed
! arguments: {:goal=>Vector3D(x=10.000000,y=10.000000,z=0.000000)}
! realized_by ComputePath:0x483502e0
! owners: Roby::Distributed
! arguments: {:max_speed=>1.0,
! :goal=>Vector3D(x=10.000000,y=10.000000,z=0.000000)}
!The following tasks have been killed:
! ComputePath:0x483502e0
! MoveTo:0x48350370
Mmmm... What happened ? The call to move_to! returned properly, which
means that the plan has been properly generated and the MoveTo high-level
action started. Nonetheless, an error occured.
The error message appeared because an ArgumentError exception has been raised
in thread_task.rb:51 Looking at the documentation of Roby::ThreadTask,
we see that the definition of ComputePath has not called the Roby::ThreadTask.implementation
statement, and as such the polling handler failed. Roby answers to that by
emitting the +failed+ event of the problematic task.
The plan-related error (ChildFailedError) has then been generated by Roby's
plan analysis:
* a +realized_by+ relation between MoveTo and ComputePath exists, which means
that MoveTo cannot be achieved without executing ComputePath first.
* ComputePath failed, so in the current state of the plan, the MoveTo
action cannot be achieved either.
A more complete description of errors and, more importantly, of how to handle
them is given in the following tutorials.
== Implementation of +ComputePath+ and +TrackPath+
The first section did mainly explain how the plan represents the logical
relations between each tasks and each task's events. We will now get into the
details of actually implementing these tasks.
* First, we have to initialize the position in tasks/PathPlan.rb
Roby::State.update do |s|
s.pos = Roby::Pos::Vector3D.new
end
* for +ComputePath+, we will simply generate a random set of points in-between
the current robot position and the specified goal. In general (i.e. not here,
but in a real case), this process takes time and as such cannot be done in
one pass of the execution cycle. We will therefore use a thread to do it,
leaving the actual thread management to Roby::ThreadTask:
# The robot position at which we should start planning
# the path
attr_reader :start_point
# Initialize start_point and call ThreadTask's start command
event :start do |context|
@start_point = State.pos.dup
super
end
# Implementation of the computation thread
implementation do
path = [start_point]
while goal.distance(path.last) > max_speed
u = goal - path.last
u /= u.length / max_speed
path << path.last + u
end
path << goal
Robot.info "#{path.size} points between #{start_point} and #{goal}"
path
end
on :success do |ev|
# Parents is a ValuSet, it has no #first method. Get
# the first element with #find
parents.find { true }.data = result
end
See Roby::ThreadTask to implement _interruptible_ external threads.
Robot is a namespace which (among other things) can be used to access an
application-specific logger set up by Roby itself. It answers to #debug,
#info, #warning and #fatal, and by default is at the INFO level. The Logger
object itself is accessible at Robot.logger. Therefore, use
Robot.logger.level= to change the logger level itself.
* as stated before, MoveTo does not require any special code. It is here
only to represent a high level activity (the whole movement), not to actually
execute it.
* TrackPath will then take the path data and execute the corresponding movement. For
the purpose of this tutorial, it will simply move to the next point in the path
at each execution cycle:
# The current waypoint
def current_waypoint; path_task.data[@waypoint_index] end
poll do
@waypoint_index ||= 0
State.pos = current_waypoint
@waypoint_index += 1
if @waypoint_index == path_task.data.size
emit :success
end
Robot.info "moved to #{current_waypoint}"
end
= Next tutorial
The {next tutorial}[link:files/doc/tutorials/04-EventPropagation_rdoc.html] will
allow you to understand more by actually seeing what happens during the plan
execution. After this tutorial, you should be able to build simple task
models and simple plans, as well as execute them and understand the most common
error -- ChildFailedError.
---
vim: tw=80 et