ROB 599: Programming for Robotics: Homework 6

Introduction

In this homework, we are going to split the different parts of homework 5 potential into three different processes that will communicate with each other through LCM messages.

To prevent conflicts when multiple students submit their code at the same time, we will append our uniqid to the channel names. This way, even if every student submits their code at the same time, their will be no conflicts since every student will be using a set of unique channels.

LCM Spy

It will be very helpful to be able to have a reliable way to see what messages you are publishing with LCM and what they say. We will use the lcm-spy tool for this.

Briefly review this section now, but it will only be useful to run once you have created some of the LCM files for this homework below and you are trying to debug your programs!

The program lcm-spy was written in Java and needs to have your LCM types compiled to Java in order to be able to decode them for you. While in your project directory, with the .lcm files in a folder called lcmtypes, run the following commands to generate Java LCM files, compile them, package them into a lcmtypes.jar file, and then run lcm-spy with them.

# Where is the java LCM library? We access it by making a path relative to the lcm-gen tool
lcm_jar=$(dirname $(which lcm-gen))/../share/java/lcm.jar
lcm-gen -j lcmtypes/*.lcm

# The Java compiler needs to use the java LCM library to properly compile these files
javac lcmtypes/*.java -cp $lcm_jar

# Now we take all of the different compiled class files, and loosely compress them together into one lcmtypes.jar file
jar cf lcmtypes.jar lcmtypes/*.class

# lcm-spy, in turn, needs access to this lcmtypes.jar file in order to decode our LCM messages
CLASSPATH=lcmtypes.jar lcm-spy >/dev/null

We can make this easier for us by putting this as a couple of entries in make. Notice that we have to make several changes because make does not inherently use bash, and we have explicitly ask it to use the shell (bash) in several places.

lcm_jar := $(shell dirname $(shell which lcm-gen))/../share/java/lcm.jar

# Just like a C program, we can express how to make the lcmtypes.jar file from a set of .lcm files
lcmtypes.jar: lcmtypes/*.lcm
    lcm-gen -j lcmtypes/*.lcm
    javac lcmtypes/*.java -cp $(value lcm_jar)
    jar cf lcmtypes.jar lcmtypes/*.class

# Running the tool lcm-spy requires this lcmtypes.jar file to exist and be up to date
lcm-spy: lcmtypes.jar
    CLASSPATH=lcmtypes.jar lcm-spy >/dev/null

With this setup, however, we can simply run make lcm-spy to run the tool, and it will automatically regenerate and recompile the java LCM types as necessary!

Remember that in a makefile you must use a tab for the indentation at the beginning of a line! If you copy and paste, you will have to change the spaces to tabs in your text editor.

Using make to generate LCM files for C

Just like with using make for the Java LCM files, we can do the same for C:

# The '%' is a wild card. To make any lcmtypes .c file, use the corresponding .lcm file
# For organization, we reference and use all these files under a lcmtypes directory
lcmtypes/%_t.c: lcmtypes/%_t.lcm
    lcm-gen -c --c-cpath lcmtypes/ --c-hpath lcmtypes/ $^

LCMTYPES_SOURCE = lcmtypes/settings_t.c lcmtypes/reset_t.c lcmtypes/world_t.c lcmtypes/agent_t.c lcmtypes/action_t.c

# Now instead of listing all the LCM type files individually, we can just add $(LCMTYPES_SOURCE) to any list of .c files

lcm_handle_async

When using LCM in a program there are several different approaches you can take to processing the messages.

Some times, you can start a thread to just run:

while (true) {
    lcm_handle(state->lcm);
}

Unfortunately, if you use this approach you have to worry about race conditions and other threading problems with how your LCM message handler functions run.

It is my personal preference to avoid making new threads whenever possible, because threading problems can be very hard to debug.

I have supplied a function lcm_handle_async can help with this. Normally, whenever you run lcm_handle that function will wait (like with nanosleep) until a message has been received. Then it will call the appropriate message handler you gave when you called subscribe on a message type. Then it will return, having processed a single message.

Because lcm_handle will wait for an unpredictable amount of time, it is not convenient to use when your program needs to be processing data at a regular rate.

As an alternative lcm_handle_async will immediately process any and all messages that have already been received, but will not wait if there are not any available. This means we could write code that looks like this (relevant to the world problem):

while (true) {
    lcm_handle_async(state->lcm);
    update motion
    update collisions
    publish lcm messages
    wait long enough to run at exactly 25 Hz
}

And when we are run the update methods, we can know that we have all the up to date information from the recent LCM messages.

Problem 1: gui

In this problem, we will isolate the features of potential that are unique to the user interface: keyboard inputs and image server outputs.

At a rate of 25 Hz (every 40 milliseconds), you should publish an LCM settings message on the channel SETTINGS_uniqid:

struct settings_t {
    int32_t initial_runner_idx;
    int32_t delay_every;
    double to_goal_magnitude;
    int32_t to_goal_power;
    double avoid_obs_magnitude;
    int32_t avoid_obs_power;
    double max_vel;
}

Put this into a file named lcmtypes/settings_t.lcm, and put all the other LCM types into similarly named files. The LCM types I give in this document need to be copied verbatim or you will get errors decoding them!

You should use seconds_now() and nanosleep together so that you can publish at a consistent rate. With seconds_now you can time how long it takes run lcm_handle_async and update_graphics and to publish messages, and then only wait 40 milliseconds minus however long those functions already took.

Whenever the user presses the r key, you should publish a reset_t LCM message on the RESET_uniqid channel:

struct reset_t {
    int32_t initial_runner_idx;
}

You may also find it convenient to send a reset message when you first run gui as well, to facilitate debugging, but this is not tested.

You should also subscribe to the WORLD_uniqid channel (see below) so that you can know where to draw the runner and chaser for the image server.

From the terminal, the gui program should behave just like potential originally did, except that it will not report the runner getting caught.

Problem 2: world

In this problem, we will implement those features that correspond to the physics of our simulated world: the runner’s random movement, movement of the chaser based on its velocity and angular velocity, and collisions.

You should subscribe to the SETTINGS_uniqid (you only need the delay_every variable) and RESET_uniqid channels. When you receive a reset message, you should immediately reset the simulation.

You should also subscribe to a new ACTION_uniqid channel that is used to communicate the chaser’s action:

struct action_t {
    double vel;
    double ang_vel;
}

When you read in the velocity and angular velocity from this action message, you still have to constrain the chaser to its maximum possible action limits. Velocity can only increase by 2.0 per timestep, and angular velocity is limited in magnitude to PI / 16.

Instead of sleeping for 40 milliseconds every delay_every time steps, divide the 40 milliseconds evenly, sleeping for 40 / delay_every milliseconds every time step. For example, if delay_every is 10. Before we would sleep for 40 milliseconds every 10 time steps. Now we will just sleep for 4 milliseconds every time step. This will allow the chaser time to choose and communicate its choice of action back to the world process.

Every time step, you should compute the movement of the agents and resolve all collisions. If the runner is caught, you should report which time step this happened on, and then reset the simulation, just like with potential. The time step numbers should be the same as before.

After these calculations, you should publish the new states of the agents to the WORLD_uniqid channel, composed of two LCM types:

struct world_t {
    agent_t runner;
    agent_t chaser;
}
struct agent_t {
    boolean is_runner;
    double x;
    double y;
    double theta;
    double vel;
    double ang_vel;
}

Problem 3: controller

In this problem, we will implement the chaser’s potential field motion controller.

You should subscribe to the SETTINGS_uniqid and WORLD_uniqid channels. Whenever you receive a message on the WORLD_uniqid channel, you should immediately compute the chaser’s appropriate action and then publish the result to the ACTION_uniqid channel.

You should start with all the default settings from homework 5, even if you don’t receive a new settings message.

This means you can actually use just a single lcm_handle loop, because you only ever do anything when you receive a world message:

while (true) {
    lcm_handle(state->lcm);
}