-
-
Notifications
You must be signed in to change notification settings - Fork 17
Teleop Starter Project
This project will involve the creation of a drive control test bench, in which you will create an override for the joystick that we use to control the rover, display the input joystick values on the GUI, send these joystick values to a backend node where they will be mathematically converted to drivetrain values, then display these drivetrain values back on the GUI.
In this tutorial any code that is surrounded by *asteriks* is code that you are meant to add to the project in a certain spot. Please make sure you understand what you are adding when you add it and ask questions as you go. This project is solely for your learning so take as much time as you need to understand what is actually happening as you go through it.
First, checkout a new branch to work on:
git checkout -b <your initials>/starter-project
example: git checkout -b jnn/starter-project
This will create a new branch so that you will not be changing your master branch in your git repository. You will not be pushing this branch, it is just there to keep it separate from your master branch. To start the project, you will launch it with a launch file much like the main base station GUI:
roslaunch mrover teleop_starter.launch
Then navigate to localhost:8080 in your web browser
Now that you have cloned the starter code and have set up your workstation, you are ready to begin the actual project. There are a few things in the starter code to take a look at.
First let's take a look at launch/teleop_starter.launch:
<launch>
<include file="$(find rosbridge_server)/launch/rosbridge_websocket.launch"></include>
<node name="gui" pkg="mrover" type="gui_starter.sh" cwd="node"/>
</launch>
This launch file can be launched using roslaunch and will run the rosbridge_server which works in very much the same way the old lcm bridge server worked. The bridge server allows the vue code to interface with the ROS master server and send messages from the GUI. This also runs gui_starter.sh, a bash script which wraps the yarn functions necessary to run the GUI.
Next let's take a look at package.json:
"devDependencies": {
"@webpack-cli/serve": "^1.7.0",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"babel-preset-es2015": "^6.24.1",
"copy-webpack-plugin": "^4.5.2",
...
This file defines the packages that yarn installs when running the "yarnpkg install" command. Webpack is what is being used in the background to build our code and make it accessible in the browser.
Next let's look at the src/components directory, as well as router/index.js. Menu.vue defines the main menu that can be found at the default route of our program.
DriveControls.vue has some basic code to get values from the Joystick that is normally used to operate the rover.
interval = window.setInterval(() => {
const gamepads = navigator.getGamepads()
for (let i = 0; i < 4; i++) {
const gamepad = gamepads[i]
if (gamepad) {
if (gamepad.id.includes('Logitech')) {
// -1 multiplier to make turning left a positive value
// Both left_right axis and twisting the joystick will turn
this.rotational = -1 * (gamepad.axes[JOYSTICK_CONFIG['left_right']] + gamepad.axes[JOYSTICK_CONFIG['twist']])
// Range Constraints
if (this.rotational > 1) {
this.rotational = 1
}
else if (this.rotational < -1) {
this.rotational = -1
}
// forward on joystick is -1, so invert
this.linear = -1 * gamepad.axes[JOYSTICK_CONFIG['forward_back']]
const joystickData = {
'forward_back': this.linear,
'left_right': this.rotational,
}
}
}
}
}, updateRate*1000)
It's encouraged that you explore other files but these are the essential spots that are important to the starter code. Let's begin working on the motor sim!
First we are going to add a separate route, /motor_sim for the drive controls vue module. A route is simply a separate page under the same website. For example, for project management we use https://github.com, but our repo is located at https://github.com/umrover/mrover-ros, "umrover/mrover-ros" is the route to our repo.
To add our route we are going to add a MenuButton to the Menu component.
<div class="box row">
<MenuButton link="#/motor_sim" name="Motor Simulator"></MenuButton>
</div>
This will create a menu button that will link to the motor_sim route. The template section of the code should now look like:
<template>
<div class="wrapper">
<div class="box header">
<img src="/static/mrover.png" alt="MRover" title="MRover" width="48" height="48" />
<h1>Mrover Teleop Training</h1>
<div class="spacer"></div>
</div>
<div class="box row">
<MenuButton link="#/motor_sim" name="Motor Simulator"></MenuButton>
</div>
</div>
</template>
Now we need to add the MenuButton to the components of the vue module. Add the following line to the top of the script section of the code
*import MenuButton from './MenuButton.vue';*
export default {
...
then add a components section to the export section
export default {
name: 'Menu',
*components: {
MenuButton
}*
}
Your menu will now look like this:
You'll likely notice that when you click the button it takes you to a blank page. This is because we need to define the route. Open the src/router/index.js file. We are going to add a route that looks very much like the one that is already present for the menu.
import Vue from 'vue'
import Router from 'vue-router'
import Menu from '../components/Menu.vue'
*import DriveControls from '../components/DriveControls.vue'*
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Menu',
component: Menu
},
*{
path: '/motor_sim',
name: 'Motor Sim',
component: DriveControls
},*
]
})
"Drive Controls" should now be displayed on the /motor_sim route.
Next we are going to send the values from the joysticks that are currently being read in in DriveControls.vue to a ros joystick topic. To do this, first we are going to create a message file. Create a new directory in the top-level of your teleop_training workspace called "msg" and inside this folder create a new file called Joystick.msg.
Your workspace should now look like this:
For returning members, this is the replacement for the old rover_msgs folder that held all of the .lcm files. Inside the Joystick.msg file we will define the members of the message we are going to be sending:
float64 forward_back
float64 left_right
Note that these message files use types according to the ROS message format defined here.
Next, we will need to add our message to CMakeLists.txt in the teleop starter folder so that catkin can build our new messages. Add the Joystick message to the add_message_files function in the starter project CMakeLists file.
add_message_files(
FILES
Joystick.msg
)
Now navigate back to the catkin_ws directory and run "catkin build" again to add your message to the mrover package.
Now we need to send our joystick values that we are getting to a ROS Topic so that other ROS node can access it. First we add an import of the ROS Javascript library to the top of the script section of DriveControls.vue
<script>
*import ROSLIB from "roslib"*
let interval;
...
Then we will declare a publisher object with type to publish to the topic "/joystick" with a message type of "mrover/Joystick"
The object will be declared in the data section and then assigned in the created() function so that only one publisher is created for each topic.
export default {
data () {
return {
rotational: 0,
linear: 0,
*joystick_pub: null* <-- Declaring variable
}
},
...
const JOYSTICK_CONFIG = {
'left_right': 0,
'forward_back': 1,
'twist': 2,
'dampen': 3,
'pan': 4,
'tilt': 5
}
*this.joystick_pub = new ROSLIB.Topic({ <-- creating the publisher
ros : this.$ros,
name : '/joystick',
messageType : 'mrover/Joystick'
});*
const updateRate = 0.05;
...
There are a few things to note about this code block. For one, all values declared in the data() section need to be prefixed with "this." when accessing them at any time. This is because they are member variables of the Vue component and thus can be accessed in any context and are not just local to a certain functon or context. Another thing to notice is the "this.$ros" in the creation of the publisher. This ROS object is the GUIs connection to the ROS master server, it is declared in app.js:
import ROSLIB from "roslib"
import router from './router'
import 'leaflet/dist/leaflet.css'
Vue.config.productionTip = false
*Vue.prototype.$ros = new ROSLIB.Ros({
url : 'ws://localhost:9090'
});*
$ros is a prototype variable: this is essentially the Vue.js version of a global variable. From within any component, we can access prototype variables by using "this.". Typically, prototype variables are prefixed with a dollar sign to differentiate them from the component member variables.
Now you need to actually use the publisher to send joystick values:
...
const joystickData = {
'forward_back': this.linear,
'left_right': this.rotational,
}
*var joystick_message = new ROSLIB.Message(
joystickData
);
this.joystick_pub.publish(joystick_message)*
}
}
}
}, updateRate*1000)
...
Now we need to make a new node that will translate our joystick inputs to actual commands. These commands will be sent to the left and right drivetrain of the rover (The ESW team takes over from there). Create a python file in the starter_project/teleop directory called teleop_starter.py. Here is some boilerplate code to get started:
import rospy
class DriveControl():
def joystick_callback(self,msg):
pass
def main():
pass
if __name__ == "__main__":
main()
Note that "pass" in Python is just a placeholder for a functon that has not yet been written and needs to be deleted when we write these functions. Also note that we created a class for drive control functions. While we will only have one funcion present here, this is a practice that we do in our code to organize our callback functions by purpose, as well as store any necessary state. To start, first we need to create a DriveControl object, declare this script as a ros node, and set up our subscript to receive our messages from the "/joystick" topic.
def main():
drive = DriveControl()
rospy.init_node("teleop_starter")
rospy.Subscriber("/joystick", Joystick, drive.joystick_callback)
rospy.spin()
Note the rospy.spin() function at the end, this just tells ROS to continue running this node and checking the subscriptions until the node is shut down manually (using ctrl+C in the terminal).
This will subscribe to the /joystick topic and run the joystick_callback function that we are about to define each time a message is received. Now we need to actually translate the joystick values to motor values. Create a new message called WheelCmd.msg to send values to the wheels.
float64 left
float64 right
Refer back to making the Joystick message if you get stuck making this wheel message.
Now Import your messages at the top of the python script
from mrover.msg import Joystick, WheelCmd
This is the part of the project where, if you would like, you can write custom code. There are many different ways that one could handle the translation of the values from joystick to motor outputs. Here is some simple code that will accomplish this based on this stackexchange post, but keep in mind that this is not the only way to do this and the purpose of the project you are building is to test different algorithms for this.
def joystick_callback(self,msg):
forward_back = msg.forward_back
left_right = msg.left_right
#Scaling multiplier to adjust values if needed
K = 1
left = K * (forward_back + left_right)
right = K * (forward_back - left_right)
#Ensure values are [-1,1] for each motor
if abs(left) > 1:
left = left/abs(left)
if abs(right) > 1:
right = right/abs(right)
Whether you choose to use this code or not, we will now send these left and right values to the /wheel_cmd topic. First, add an init function to the DriveControl class and create your publisher object for the wheel outputs.
class DriveControl():
def __init__(self):
self.wheel_pub = rospy.Publisher("/wheel_cmd",WheelCmd,queue_size=100)
...
Then, at the end of the joystick_callback function add this code to publish your wheel outputs:
#Create a new wheel message object
wheel_out = WheelCmd()
wheel_out.left = left
wheel_out.right = right
self.wheel_pub.publish(wheel_out)
Now your teleop_starter file should look something like this:
import rospy
from mrover.msg import Joystick, WheelCmd
class DriveControl():
def __init__(self):
self.wheel_pub = rospy.Publisher("/wheel_cmd",WheelCmd,queue_size=100)
def joystick_callback(self,msg):
forward_back = msg.forward_back
left_right = msg.left_right
#Scaling multiplier to adjust values if needed
K = 1
left = K * (forward_back + left_right)
right = K * (forward_back - left_right)
#Ensure values are [-1,1] for each motor
if abs(left) > 1:
left = left/abs(left)
if abs(right) > 1:
right = right/abs(right)
#Create a new wheel message object
wheel_out = WheelCmd()
wheel_out.left = left
wheel_out.right = right
self.wheel_pub.publish(wheel_out)
def main():
drive = DriveControl()
rospy.init_node("teleop_starter")
rospy.Subscriber("/joystick", Joystick, drive.joystick_callback)
if __name__ == "__main__":
main()
Now we add this node to the teleop_starter launch file in order to run it alongside the GUI and bridge server
<launch>
<include file="$(find rosbridge_server)/launch/rosbridge_websocket.launch"></include>
<node name="gui" pkg="mrover" type="gui_starter.sh" cwd="node"/>
*<node name="teleop_starter" pkg="mrover" type="teleop_starter.py"/>*
</launch>
And make the node executable with chmod in the terminal
cd starter_project/teleop
chmod +x teleop_starter.py
The final step will be to subscribe to the /wheel_cmd topic on the GUI end of things and display the output values that are given for the motors in order to observe the outputs of whatever algorithm you chose.
Head back to DriveControls.vue to begin on this.
The first step to doing this will be adding variables to hold the most recent wheel_cmd values.
export default {
data () {
return {
rotational: 0,
linear: 0,
*left: 0,
right: 0,*
joystick_pub: null
}
},
Now we will display these values on the GUI.
<div>
<p>Drive Controls</p>
<div>
<p>Left Motor Output: {{left}}</p>
<p>Right Motor Output: {{right}}</p>
</div>
</div>
Putting things in double curly braces will output the member value of the Vue component with the same name. Note that since this only works for member components, you do not need to use "this."
Now we need to subscribe to the /wheel_cmd topic and assign it's values to left and right as each message is received.
...
this.joystick_pub = new ROSLIB.Topic({
ros : this.$ros,
name : '/joystick',
messageType : 'mrover/Joystick'
});
*this.wheel_cmd_sub = new ROSLIB.Topic({
ros : this.$ros,
name : '/wheel_cmd',
messageType : 'mrover/WheelCmd'
});
this.wheel_cmd_sub.subscribe((msg) => {
this.left = msg.left
this.right = msg.right
})*
const updateRate = 0.05;
...
The parameter that is listed in the subscribe function is, itself, a callback function that processes the message as it is received.
You may have been asking yourself through this starter project "I don't have a joystick, how am I going to test this code?" Fear not, ROS has an answer. The rostopic command line utility allows us to send our own custom messages to any ros topic using only the terminal.
You can send a Joystick messages by entering:
rostopic pub /joystick mrover/Joystick '{forward_back : 1.0, left_right: 0.0}'
rostopic is a super helpful tool for debugging, the other most important command to know about is rostopic echo. After sending your joystick message run the following command in another terminal:
rostopic echo /wheel_cmd
This should display the same values that are displayed on the GUI end of things. You can learn more about rostopic here.
Your final Motor sim screen should look something like this:
It is highly reccomended that you use rostopic and send several /joystick messages and think about whether the outputs make sense for our rover. Remember that the rover turns by having different values sent to each side of the drivetrain and not by steering like a car.
If possible, you can go try this out with the same Logitech joystick we use to control the drivetrain on the rover!
With this, you have finished the teleop starter project and hopefully should feel comfortable writing code that can be used to control the rover. Congratulations!