Skip to content

Teleop Starter Project

jnnanni edited this page Sep 6, 2022 · 19 revisions

The Starter Code

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 to take note of in the starter code to take a look at.
First let's take a look at launch/teleop.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 allowing us to send messages from the GUI. This also runs gui_starter.sh, a bash script which wraps the yarn functions neccesary 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 pack 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!

Adding a route for the drive controls

First we are going to add a separate route, /motor_sim for the drive controls vue module. To do this 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:
image
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

Adding a custom ROS Topic Message

Next we are going to send the values from the joysticks that are currently being gotten in the interval 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:

image

For any 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.

Publishing to a ROS Topic in Vue

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 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. In 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)
...

Creating a node to transform joystick values to motor outputs

Now we need to make a new node that will translate our joystick inputs to the actual values that will be sent to the left and right drivetrain of the rover. 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 presents here, this is a practice that we do in our code to organize our callback functions by purpose. To start, first we need to 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

Adding motor Outputs to the GUI

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.

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 assing it's values to left and right as they come in.

...
  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.

Testing your Motor Simulator

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.

Final Outputs

Your final Motor sim screen should look something like this:
image
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 opposite values sent to each side of the drivetrain and not by steering like a car.

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!

Clone this wiki locally