Usually these course lessons are set up so that I spend one lecture going over a topic and then we have
a project which applies it.
In this lesson, we will be doing a project walkthrough to explain how to utilize actions in ROS two.
ROS actions are not inherently complicated, but they have a lot of parts to them, so it's easier to
understand when taught in the context of using it in a robotics project.
As you learned in the Ross two overview video, Ross actions is another inter node communication method.
Here in action goal is similar to a service request and an action result is similar to a service response.
The feedback aspect of an action is similar to a publisher in which it keeps publishing feedback relevant
to the state of the goal until a result is reached.
In this project walkthrough, we will program similar functionality shown in the Ross overview animation.
The project scenario is that we have a mobile camera robot and we want to send it to coordinates to
a goal point to navigate to.
While the robot is navigating, we want it to send feedback on how far away it is from the goal point.
When it reaches the goal, the robot then returns a result of how long it took to get to that goal point.
This is an ideal situation where we'd want to use a Ross action.
It has a goal aspect, feedback aspect and a result aspect to it.
An additional thing to note is we will also need to create a publisher to simulate a position sensor,
which tells us the current point the robot is at.
Our action will find the distance between the goal point and the simulated current position to send
feedback on how far the robot is from the goal.
OC now lets you understand the idea behind this project.
Let's walk through it together.
All right, so how do we go about creating a custom action interface?
Well, it's quite similar to how we create our service.
First, we need to create a custom action file.
This will be placed within our package folder.
So create a new folder called Action.
And then within this folder, we can create our actual action file.
In this case, I'm going to call Mines Navigate Action.
There are three sections to this file, and I'll quickly comment out here to make this file more readable.
The first is the goal.
Just like services, we will separate each section with three dashes.
The second section is the result and the third section is the feedback.
My goal for this project is a point at which I want the robot to travel to.
I'll do this project in terms of a2d grid with X and Y coordinates, rather than creating a custom x
and Y part to my request, I can do a quick search to see Ross message types available that may be able
to help me.
So here I'll be using the point message type from the Geometry Messages library.
So we see this pre-made message type has a float 64 data type for x, y and z coordinates, so I'll
go ahead and utilize this rather than creating something custom.
To include this in my action file.
I will simply type geometry messages slash point that I'll name the attribute of my goal Goal point.
My result of this action will be the time that elapsed while I travel to the goal point.
I could use one of Roz's time based messages, but I'll keep it simple and just use a float 32 message
type to return back the elapsed time in seconds.
Lastly, my feedback will just be the distance my robot is from the goal point.
I can also keep this simple and just use a float 32 message type to return back the distance in meters.
Assuming that my grade I use is in meters.
Great.
That's all we need for our action file, but we'll need to make sure that our package is set up to create
custom action interfaces.
If I open up my package XML file, I already have most of the penances I need from these services lecture
related to creating custom interfaces by including Ross Idle.
The only additional thing I need to do is add dependencies for action messages like so that's it.
I can save this file and close it.
Now I'll have to head over to my quick List text file to let it know that I've made a new interface
that it needs to compile.
All I need to do for this is to add the file to my ROS.
Idle generate interfaces function.
In this case it is action forward slash navigate dot action.
Now I'm also using the geometry messages package as a dependency for my action, so I'll be sure to
include that as a dependency here as well as to find the package above.
All right.
With that, I can save the file and be sure to save my action file, and we can go ahead and rebuild
our workspace.
No.
And I made a typo here where I forgot the s and geometry messages.
So I'll just go ahead and save that and we can go ahead and try to rebuild.
All right, great.
So we successfully built our workspace.
So now if I head over to terminal, we can go ahead and source it.
And now if we do Ross to interface lists under actions, we see our Udemy rescue package action navigate
that we just created.
As usual, you can always find information about the action by doing Ross to interface show followed
by the name of the action.
And Odysseus has gone ahead and broke down our action into its dependency core components.
So here we are using the geometry messages, Gold Point, and it broke it into its individual X, y,
z coordinates.
So that's one of the cool things when utilizing that interface show command is you'll always get the
base units for each of the corresponding attributes.
Great.
With that, I'll go ahead and clear the screen and we can go ahead and start working on our action server
so I can go over the scripts, create a new file and I'll just call it Action Server PI.
We we'll start this group the same way we have been doing throughout the course by including our Ross
modules and creating our main function.
To make this quicker, I'll simply copy and paste the contents of our original publisher note.
So here, I'll just go ahead and change the class name to navigate action server.
And change some of the corresponding print statements and variable names.
In order to use the action server functionality, we must import it in from RC LP action.
Then we will include our custom action interface we just made.
We do that the same way we did with our custom service interface.
Now we will create an action server variable within our class, which we will set equal to the Ross
Action Server object.
A common thing programmers do within class variables is start them with an underscore if the name is
similar to other content in the code.
That is why I name this variable underscore action server as that way.
We know this is associated with the classes action server.
Now the action server call takes in four parameters.
The first is the node.
This navigate action server class is the node, so we can just pass in self.
Then we'll pass in the action interface, which in this case is my custom navigate action.
And the third argument is the action topic name, which I'll just call navigate.
Then the fourth argument is the callback, which runs when we receive a goal from the client, which
I will just call, navigate, callback.
Let's go ahead and create this callback function below.
It will take in self and the action server goal handle.
I'll just start this off by printing out that we got a goal.
So the main thing we're going to want to do from the goal is to unload the point we received from the
client.
So I will create a variable called robot goal point and set it equal to a list.
And I will unload each coordinate by calling goal handle dot request followed by the name of our request
attribute, which, as you can see in my navigate action file, I called it Goal point.
Then I will specify the x coordinate.
I can then go ahead and do this for the Y and z coordinates as well.
Then to make sure we are unloading this request correctly, I will just print it out to our terminal.
OC.
We have our goal now.
We need to calculate the distance between the current position of our robot and this goal position.
In order to get the current position of the robot, I will need to first create a subscriber to get
the current robot position.
To do this I will create a sub variable and use the create subscription function of our node.
This subscriber will subscribe to a point message from the Geometry Messages library, so I will need
to include it above.
Then I'll just say the topic I will be subscribing to is robot position and each time a message gets
published to the topic we will run a callback function called update Robot position.
Then I will set my queue size, which I'll just set to one as I only want to process the latest position
received.
Now let's create this callback function.
All I wanted to do is update the variable accessible by my action server callback function, so I'll
just create a variable called robot current position.
This will be a list.
I unload the x and y and z coordinates of the point to.
For good measure.
I should make sure we don't try to calculate the distance if we have not received the robot current
position.
So I will initialize this variable as none and in my action server callback make a while loop which
will hold us up until we receive a point.
I will simply prompt the user that our robot point was not detected.
And I will use the spin one function which takes in our node and I can set the time out period, which
I'll just set to 3 seconds.
Now there's a specific reason why I use the spin one function instead of importing the time Python module
and calling time sleep.
If you use time sleep, it will block any other code from being run until it finishes and then it will
continue its current thread.
That means our subscriber would be blocked and never be able to receive any new messages.
So we use the spin of one function within this while loop, which basically allows all of our other
ROS functionality within this class to run while we wait.
However, this brings up another problem as we are currently using this spin function in our main function.
Calling spin once within a spin instance will generally lead to getting stuck.
To prevent this, I will instead replace this spin call with spin once call within a while loop and
I will use the kl pi oc function which returns true as long as this node is not killed.
To keep this while loop running.
If you're interested in learning more about how spending affects your code, feel free to check out
the code API documentation on it.
While we are here within the ACCEPT statement, I will also call the destroy method of our action server
variable within the node class.
This will properly shut down the action server by letting any clients who sent requests know it's shutting
down before the node goes and terminates.
All right, let's go back up to our callback function where we're processing our navigate callback.
Once our subscriber receives the robot position, we can go ahead and calculate the distance between
the two of them.
I can do this with the math libraries dist function.
Then we're going to want to publish feedback of this distance until it is zero, or at least within
a certain threshold distance.
I'll just set a constant called distance threshold at the top of my script and I'll just set it equal
to 0.125, which means that if I'm within 0.125 meters of the goal, I am basically at the goal and
I've achieved this action.
I will implement this constant in a while loop like so to keep calculating the distance until we get
within the distance threshold.
Every time we loop, I'm going to recalculate the distance and fill the feedback message with the data.
So I will initialize the feedback message variable right before the while loop.
Great.
So in this while loop, we'll recalculate the distance.
So I'll just go ahead and copy this over and then take the feedback message and load the distance data
into the distance of the point attribute, which I named in my action file.
Then to publish the feedback, I will call the goal handle publish feedback method which takes in the
feedback message object.
Then just so that we are not publishing feedback as quickly as possible, we can use the spin once function
again to essentially set a rate to publish the feedback without blocking the operations of our subscriber.
I'll set the timeout second rate to one second.
All righty.
That about wraps it up for feedback.
Now we can move on to sending the result once we reach the goal point.
The first thing I will do once this happens is called the Goal Handle success method to let the action
server know the goal was processed successfully.
Then we will initialize our result message.
Then we need to fill the result message.
With this time elapsed from when we started the goal.
So first I will create a variable at the beginning of our action server callback called start time.
You could use the time library for this, but I'll just use the get clock now method of our node.
In order to use this properly, you also need to convert this from the time object to a message.
So I will call to message.
Then I'll go back down and load the elapsed time attribute of my result message as I specified in my
action file and set it equal to the current time minus the start time and I'll make sure the result
is a float by wrapping it within float tags.
Last but not least, to close this whole function out, we will return the result.
With that, our action server node is complete.
So let's go ahead and review what's going on here.
We created an action server which triggers the navigate callback to process the goal and a subscriber
to update the robot's current position variable.
Within our navigate to callback, we receive the data from the goal request check to make sure we receive
the robot's current position, calculated the distance between the robot and the goal, and then continue
to publish that as feedback until we are within the distance threshold.
Once within the distance threshold we call the goal handle success method load our result message data
and return it.
Along the way, we also discussed how to utilize the spin one's function to make sure we are not blocking
other parts of our node from running.
In this case, all of our while loops within our action callback used spin ones to make sure we did
not block the subscriber from receiving new data.
Great.
With that, let's save the file.
Oh, and I made a mistake here where I forgot to put a comma after my point y.
And there we go.
Now, that got rid of that particular error with my python list.
So let's go ahead and save the file and we can move on to the action client node.
So I'll go into my scripts folder, create a new file, and I'll just call it Action Client.
Bye.
So from here, I'll just go ahead and copy the majority of the content within our action server node.
But I can go ahead and delete most of the content within the class.
And I can get rid of our distance threshold, constant variable, and go ahead and rename our navigate
action server class and node names to represent a client instead.
And since this client won't have a server to destroy, I can just go ahead and delete that line as well.
So all we have here is our corresponding imports to utilize our actions a base node class, and then
we're initializing our RC LP and creating our node.
So the only thing I'll change here is instead of importing action server, we'll instead of be using
the action client.
So let's go ahead and initialize this client within our node class.
So in this case we'll need to specify the node, which in this case is self, the message type, which
will be our custom navigate interface and then the action topic name, which just has to match our server,
which was navigate.
Oh, and I'm getting an error here because my indentations are not matching up, so I'll be sure to
configure that.
Here we go.
Now let's go ahead and create a function to send out our desired goal.
I'll just call mine, send goal, and I'll make it so that the user can pass the x and Y coordinates
to set the goal too.
The first thing we will do is create our goal message and load it in with the data we want to send.
Make sure the data you load in it matches the data type you set in your action file.
In this case, I'm making sure all my inputs are floats.
OC.
We loaded our goal message data, but before we send the message, we should make sure the server is
ready to receive it, which we can do with the wait for server function of our action client.
Once the server is ready, we can send the goal to do so.
I will use the send goal async function which returns a future variable.
This function takes in the goal message we just made and we have some other optional parameters.
I will use the feedback callback parameter to specify a callback function I will make to process the
feedback we get back from the action server.
We can now create this feedback callback function below.
This takes in the feedback message.
Whoops, I've gone ahead and have this in the wrong indentation spot.
Let's go ahead and do that.
In order to get the actual feedback object, we will need to call the dot feedback attribute of the
message.
For the most part, all I'm going to do with this feedback is print it to the terminal.
So I will print the feedback distance to point, which is my attribute name in my action file.
We have our feedback function set up, but what happens when the action server finishes processing the
request?
Well, I can take the future variable from our send request and call the add done callback function,
which takes in a callback function.
I will create a new function called goal response callback.
Now I'll create the function below which takes in the future object.
Now this is where things can get a bit confusing before we can process the result the action server
sends.
After processing our goal, we need to first make sure that it is first accepted our goal so the server
will send us this response to tell us so.
So from this future object, I'm interested in getting the goal handle which will be given to us if
we call the result method of it.
Keep in mind this is not the result of our action.
This is the result of a future object when it's done being processed.
So now we need to check if the goal was accepted.
So I will create an IF statement to see if the accepted attribute of our goal handle is true or false.
If it's false, then I'll prompt the user that the goal was rejected and we will want to break out of
this function.
So I'll just return none else.
We are just going to carry on.
I will print that the goal was accepted just so our user knows Now I will need to create another future
variable to retrieve the future object from the result portion of the goal handle.
Now, just like we added a done callback for my single future, I will need to do so for this get result
future.
I'll just create a callback function called Get result callback.
This callback will actually be responsible for processing the result.
We get back from the action server when it completes the goal.
It will be set up similarly to that of our goal response callback in the sense that it takes the future
object and we will need to get the result of the future object, which will also contain the actual
result of the action.
So also a bit confusing here as far as wording goes, but essentially we're getting the result of the
future object, which in this case has the result of our action.
Then lastly, I will process this result message.
As usual, I am simply just going to print it to the terminal.
I will print the elapsed time attribute of the result, which is the attribute name I gave it in my
action file.
Oh, and I made a typo here where I capitalized my result in get result callback.
And since I want this group to end, once we get the result, I can just terminate our node, which
will stop this band function within our main function.
All right.
That was a lot to go through.
So let's review what is happening in this code.
We created our navigate Action client node.
And here we created an action client which utilizes our custom navigate interface and listen to the
Navigate action topic name, which matches what we have here in our server.
Then we created a send goal method of our node, which takes in an X, Y, and Z point and configures
our goal weights for the action server to become available and then sends the goal to the action server.
We then set up our future callbacks for our feedback as well as our response.
So when we receive feedback from the action server, we just go ahead and print it to the screen.
Then we have a goal response callback.
So this is the callback which gets returned to us once the action server has decided whether to accept
or reject our goal.
If our goal was accepted, then we added a done callback so that once our action is completed, we can
process the result that gets sent back, which in this case we're just printing the elapsed time in
seconds and shutting down our node.
And since we're trying to do this as asynchronously as possible, we've been utilizing a lot of future
objects which have the result aspect of it.
So whenever we see future result, that just means we're waiting for this future object to get completed
to return us the result.
And only when we call feature result result are we actually working with our result of our particular
action.
So hopefully you have a general understanding of what's happening in this action client node.
So let's go ahead and actually call our send goal function within our main function, which takes in
our x, Y, and Z coordinates.
So I'll go ahead and get rid of this spin one function and instead run our end goal call.
Now we have to actually read in these X, Y, and Z coordinates from the user by adding input calls.
So here we're prompting the user in the terminal to enter an X coordinate, a y coordinate and a Z coordinate,
which we load into these X, y, Z variables, and then we go ahead and send it to our action client
using the send goal function.
All right.
With that, I'll go ahead and save this file.
Oh, and the last thing I forgot to do was spin our node after we send the goal to make sure it's stays
up while our server is processing the action.
All right.
So I did a quick review to make sure I didn't have any errors or typos.
And I actually realized instead of calling the destroying node of our node instance, what I actually
meant to call was RC LP shutdown.
So this will shut down our ROS client libraries communications, which will stop our instance from spinning
our node and we'll shut down this node once you receive our results.
So I'll go ahead and make that change and save the file.
Another typo I noticed was in Navigate action.
I actually misspelled elapsed time, so I swapped the P and the A.
So this is just supposed to be elapsed time like that.
So I'll go ahead and save that as well.
All right.
I'll go ahead and save the file and compile our workspace.
It's gone ahead and compiled so we can go over to our terminal and I'll make sure it's source.
And now we can go ahead and run our action server node.
So our action server is running.
So I'll open a new terminal tab and start up our action client.
Make sure we're sourced.
All right.
So here we get prompted to put in our X, Y, and Z coordinates.
I'll just go ahead and put two for x, three for Y, and I'll just put zero for Z.
So we see we received the goal and it was able to decipher the goal point correctly.
And here we're seeing robot point not detected, and that's because we're not publishing anything to
our robot position topic.
So if I open up a third terminal tab.
We do.
Ross to topic list.
We can see that robot position topic, which was part of the subscriber we created in our server here.
So in order to calculate the distance, we need a position of our current robot.
So if I head back to the terminal, we can use the Ross two topic pub command to manually publish these
values instead of creating a new node.
So this takes in the name of the topic, which in this case is robot position.
And then we have to put in the message type, which is the geometry messages point.
And then we're going to go ahead and put in the value, which I can do by using double quotes and then
a set of curly braces.
And we can set the individual X, Y, and Z attributes.
So I'll just go ahead and put it at the origin.
And I will also use the dash dash one flag so that we're only publishing this value once and it's not
continuously publishing and it'll be printing values to our screen.
So go ahead and run this.
All right.
And now we see we are receiving feedback from our server, which is the given distance from the goal
point to our current position, which is at the origin.
So now I can go ahead and rerun this command and change up some of the values to move us closer to the
point.
For example, if I set X to one and Y to two.
We can see our feedback updates since we got closer to the point and then we can put ourselves close
enough to the point within the distance threshold.
For example, if I do 1.9 and we'll just say 3.05, we are within our 0.1 to 5 meter limit and we get
returned our results back from our action server and we can continue to send multiple new goals back
to our action server so I can set a new one back to the origin.
So we see here we received a new goal at the origin point and I can just go ahead and set us back there.
So we get this feedback of zero and we get our result.
All right.
That was a lot to take in.
But here we have successfully implemented a ROS action in which our client sends a goal point and the
server subscribes to the robot position to calculate the distance which it sends as feedback, and then
finally sends a result of how long it took to complete that action.
In this case, we just manually change the position of the robot to get closer to the point.
But surely you can see how this ROS action framework can be applied in real life, such as commanding
a robotic arm to pick up a particular object and place it somewhere.
In this case, your goal consists of an object for the robot to pick up and the location to place it.
Then within your server command the autonomous code to actually make your robot do the action.
All the while publishing feedback such as the status of the job and eventually receiving a result.
Summary if the job was completed successfully or not.
But nonetheless, congratulations on implementing your very own Ross to action.