MRC/Tutorials/The EMC environment: Difference between revisions

From Control Systems Technology Group
Jump to navigation Jump to search
Tag: 2017 source edit
Update text for readability
Line 1: Line 1:
===The main loop===
===The main loop===


When we want to control and monitor a piece of hardware, we often want to perform a series of steps, computations, procedures, etc., in a repetitive manner. When we talk about doing something repeatedly in software, the first 'control flow statement' that comes to mind is a loop. So, lets' create a loop!
When controlling and monitoring hardware, we often need to execute a series of steps, computations, and procedures in a repetitive manner. In software, the most common way to achieve this is by using a '''loop''', a fundamental control flow statement. Let's create one!


<code>
<code>
Line 14: Line 14:
     return 0;
     return 0;
  }
  }




Line 19: Line 20:




Remember that while the condition in the while statement is true, the body of the while loop will be executed. In this case, that is forever! Fortunately you can always interrupt the program using ctrl-C from the command line. By the way, the default behavior is that this directly kills your program, so all statements after the while loop (if there were any) would never be executed. You can verify this by putting a print statement there. You will see it is never called...
 
Remember a while loop continues executing its body as long as the while statement is true. In this case, it would be running forever! If your loop lacks a stopping condition, the program will keep running until manually interrupted. You can always stop execution using '''Ctrl+C''' in the command line. By default, this forcefully terminates the program, meaning any statements after the loop (if there were any) will never execute. You can verify this by adding a print statement after the loop. You'll notice it never gets printed....




So, it's a nice loop, but there's at least three things wrong with it:
So, it's a nice loop, but there's at least three things wrong with it:
 
#It runs forever, never 'gracefully' shutting down (only by user interruption)
# It runs forever, never 'gracefully' shutting down (only by user interruption)
#It runs as fast as possible!
#It runs as fast as possible!
#It doesn't do much useful except for flooding the screen...
#It doesn't do much useful except for flooding the screen...
 
For now, let's focus on the second issue. Your operating system schedules the execution of programs, allowing multiple processes to share CPU time. When our program gets its time slice, it immediately starts printing 'Hello World!' as fast as it can. This is like a hyperactive child demanding all of your attention at once! We can do better.
For now, let's focus on point 2). Your operating system schedules the execution of programs: if multiple programs are running simultaneously, it gives each program a short period of time to perform their executions and then jumps to the next. What our program does in that time slice is printing 'Hello World!' as fast as it can! It is like a horrible, zappy kid taking up all of your time as soon as you give it some attention. We can be better than that.
 
===Sleep===
===Sleep===


Line 49: Line 48:
     return 0;
     return 0;
  }
  }




Line 54: Line 54:




Ahhh, that's much better! Now approximately every second a statement is printed. This will use way less CPU power that the previous implementation. Note that we explicitly stated 'approximately'. The loop runs at approximately 1 Hz because:
Ahhh, that's much better! With a small delay, statements are now printed approximately every second. This will use way less CPU power that the previous implementation. However, '''approximately''' is the key word here. The loop runs at roughly 1 Hz due to the following reasons:
 
# Other statements inside the loop take time to execute  (In this case as printing)
# The operating system cannot guarantee exact sleep timing due to scheduling, priority handling, and other system processes.


#The other statements in the loop also take time (in this case the printing).
The second issue is less important for now since it only becomes relevant when working with high-performance mechatronic systems. However, the first point is something you should consider. As your program grows and executes more instructions within the loop, the processing time will increase, and the fixed sleep duration to eventually cause your system to start lagging behind at some point.
#The operating system can not guarantee that it will wake you up in exactly the time specified. This has to do with program priorities, schedules, etc. In high-performance mechatronics system, it is often needed that this frequency can be specified as 'hard' or 'strict' as possible. Therefore these machines often run real-time operating systems that will guarantee that, or at least to some extent. Don't worry about it, you won't notice Ubuntu is not hard real-time.


Although you shouldn't worry about the second point, it is important to take the first into account. As you will put more and more code into the body of your application, it will take more and more time to process it. Sleeping for a fixed amount of time causes your system to start lagging behind at some point.
Fortunately, we created something for you: we provide a '''time-tracking class''' that ensures the loop maintains a consistent execution rate. Instead of sleeping for a fixed duration, it calculates the time already spent in the loop and only sleeps for the remaining required time. This prevents performance drift and keeps execution closer what you want from it. Use it like this:


Fortunately, we created something for you: a class that can be used to keep track of the time spent since the last sleep statement, and which will only sleep the remaining loop time. Use it like this:






<code>
<code>
  #include <iostream>
  #include <iostream>
  #include <emc/io.h>
  #include <emc/io.h>
  #include <emc/rate.h>
  #include <emc/rate.h>
Line 118: Line 119:
     return 0;
     return 0;
  }
  }


</code>
</code>
Line 124: Line 126:
First, try to understand what is going on. You should already be able to derive what will happen if this program is executed.
First, try to understand what is going on. You should already be able to derive what will happen if this program is executed.


Now, note a few things: The IO connection is created by just constructing an emc::IO object, it's that easy! We will loop as long as the connection is OK. Then, we can send a command to the base by simply calling a function on the io object.  
Now, note a few things: The IO connection is created by just constructing an emc::IO object, it's that easy! We will loop as long as the connection is OK. Then, we can send a command to the base by simply calling a function on the io object.


We do this at a fixed frequency of 10 Hz.
We do this at a fixed frequency of 10 Hz.


Now, fire up the simulator and visualization, and run the example. And what do you see? Voila, we move the robot using our application! Try modifying the sendBaseReference arguments, and see how they affect the robot behavior.
Now, fire up the simulator and visualization, and run the example. And what do you see? Voila, we move the robot using our application! Try modifying the sendBaseReference arguments, and see how they affect the robot behavior.




Line 147: Line 150:
     // ... We got the laser data, now do something useful with it!
     // ... We got the laser data, now do something useful with it!
  }
  }
</code>
</code>


Line 162: Line 166:
     double timestamp;
     double timestamp;
  };
  };
</code>
</code>The <code>range_min</code> and <code>range_max</code> values define the smallest and largest measurable distances. Any distance reading outside this range, either below <code>range_min</code> or above <code>range_max</code>, is considered invalid. The <code>angle_min</code> and <code>angle_max</code> values specify the angles of the first and last beams in the measurement. The <code>angle_increment</code> represents the angular difference between consecutive beams. However, this value is redundant, as it can be derived from <code>angle_min</code>, <code>angle_max</code>, and the total number of beams.


The range_min and range_max values define what the smallest and largest measurable distances are. If a distance reading is below range_min or above range_max, that reading is invalid. The values angle_min and angle_max determine the angle of the first and last beam in the measurement. The value angle_increment is the angle difference between two beams. Note that it is actually superfluous, as it can be derived from angle_min, angle_max and the number of beams.
The actual sensor readings are stored in <code>ranges</code>, which is an <code>std::vector</code> of float numbers. Each element in this vector corresponds to a distance measurement in meters at a specific angle. The angle for each reading can be calculated using <code>angle_min</code>, <code>angle_increment</code>, and the index of the vector element.


The actual sensor readings are stored in ranges. It is an std::vector, a vector of values which, in this case, stores floats. Each vector element corresponds to one measured distance in meters at a particular angle. That angle can be calculated from angle_min, angle_increment and the index of the element in the vector.
Lastly, the <code>timestamp</code> indicates when the data was recorded. It is represented in Unix time, i.e., the number of seconds since January 1, 1970. While the absolute value of the timestamp is not necessarily important, it can be useful for tracking the laser data over time or synchronizing it with other data sources, such as odometry.


Finally, the timestamp specifies at which point in time the data was measured. The timestamp is in Unix time, i.e., the number of seconds since 1 January 1970. Note that the absolute value is not necessarily important, but that the timestamp can be handy to keep track of laser data over time, or to synchronize it with other input data (e.g., the odometry data)
====Odometry Data====


====Odometry Data====
The robot has a holonomic wheelbase, consisting of two wheels arranged in a differential drive configuration, and a rotational joint that allows the entire body to rotate. This particular wheel configuration allows the robot to move both forward and sideways, as well as rotate around its axis. Each of the wheels and the rotational joint is equipped with an encoder that tracks the rotations of that wheel. By using all three encoders and knowing the specific wheel configuration, we can calculate the robot's displacement and rotation from its initial position. In other words: we can calculate how far the robot drove and how far it rotated since it's initial position. This process of calculating translation and rotation based on the wheel encoders is known as odometry.


The robot has a holonomic wheel base which consists of two wheels in a diff drive configuration and a rotational joint which rotates the entire body.  The specific configuration of the wheels allows the robot to move both forwards and sideways, and enables it to rotate around its axis. Both wheels and the joint have an encoder which keeps track of the rotations of that wheel. By using all three encoders and knowing the wheel configuration, the displacement and rotation of the robot can be calculated. In other words: we can calculate how far the robot drove and how far it rotated since it's initial position. This translation and rotation based on the wheel encoders is called odometry. However, note that this information is highly sensitive to drift: small errors caused by measurement errors and wheel slip are accumulated over time. Therefore, relying on odometry data alone over longer periods of time is not recommended! Also note that the odometry data does not start at coordinates (0,0).
However, it's important to note that odometry is prone to drift. This means small errors from measurement inaccuracies and wheel slippage can accumulate over time, making it unreliable over longer periods. As such, it's not recommended to rely solely on odometry data for long-term navigation. Additionally, keep in mind that odometry data doesn't start at coordinates (0,0).


To obtain the odometry information, do the following:
To obtain the odometry information, do the following:
Line 183: Line 187:
     // ... We got the odom data, now do something useful with it!
     // ... We got the odom data, now do something useful with it!
  }
  }
</code>
</code>


Line 196: Line 201:
     double timestamp;
     double timestamp;
  };
  };
</code>
</code>


Here x, y and a define the displacement and rotation of the robot since the previous measurment, according to the wheel rotations. The translation (x, y) is in meters. The rotation, a is in radians between -pi and pi. Like the laser data, the odometry data also contains a timestamp which is in seconds (Unix time).
Here <code>x</code> , <code>y</code> and a define the displacement and rotation of the robot since the previous measurment, according to the wheel rotations. The translation (x, y) is in meters. The rotation, <code>a</code> is in radians between -pi and pi. Like the laser data, the odometry data also contains a <code>timestamp</code> which is in seconds (Unix time).

Revision as of 11:07, 3 April 2025

The main loop

When controlling and monitoring hardware, we often need to execute a series of steps, computations, and procedures in a repetitive manner. In software, the most common way to achieve this is by using a loop, a fundamental control flow statement. Let's create one!

#include <iostream>

int main()
{
    while (true)
    {
        std::cout << "Hello World!" << std::endl;
    }
    return 0;
}



Remember a while loop continues executing its body as long as the while statement is true. In this case, it would be running forever! If your loop lacks a stopping condition, the program will keep running until manually interrupted. You can always stop execution using Ctrl+C in the command line. By default, this forcefully terminates the program, meaning any statements after the loop (if there were any) will never execute. You can verify this by adding a print statement after the loop. You'll notice it never gets printed....


So, it's a nice loop, but there's at least three things wrong with it:

  1. It runs forever, never 'gracefully' shutting down (only by user interruption)
  2. It runs as fast as possible!
  3. It doesn't do much useful except for flooding the screen...

For now, let's focus on the second issue. Your operating system schedules the execution of programs, allowing multiple processes to share CPU time. When our program gets its time slice, it immediately starts printing 'Hello World!' as fast as it can. This is like a hyperactive child demanding all of your attention at once! We can do better.

Sleep

In fact, you can tell the operating system that you're done for some time. This allows it to schedule other tasks, or just sit idle until you or another program wants to do something again. This is called sleeping. It's like setting an alarm clock: you tell the operating system: wake me up in this-and-this much time.

So, let's add a sleep statement:

#include <iostream>
#include <unistd.h> // needed for usleep

int main()
{
    while(true)
    {
        std::cout << "Hello World!" << std::endl;
        usleep(1000000); // sleep period of time (specified in microseconds)
    }
    return 0;
}



Ahhh, that's much better! With a small delay, statements are now printed approximately every second. This will use way less CPU power that the previous implementation. However, approximately is the key word here. The loop runs at roughly 1 Hz due to the following reasons:

  1. Other statements inside the loop take time to execute (In this case as printing)
  2. The operating system cannot guarantee exact sleep timing due to scheduling, priority handling, and other system processes.

The second issue is less important for now since it only becomes relevant when working with high-performance mechatronic systems. However, the first point is something you should consider. As your program grows and executes more instructions within the loop, the processing time will increase, and the fixed sleep duration to eventually cause your system to start lagging behind at some point.

Fortunately, we created something for you: we provide a time-tracking class that ensures the loop maintains a consistent execution rate. Instead of sleeping for a fixed duration, it calculates the time already spent in the loop and only sleeps for the remaining required time. This prevents performance drift and keeps execution closer what you want from it. Use it like this:



  #include <iostream>
#include <emc/io.h>
#include <emc/rate.h>

int main()
{
    emc::IO io; // Create IO object, which will initialize the io layer
    emc::Rate r(3); // set the frequency here
    while(true)
    {
        io.speak("Hello World! ");
        r.sleep(); // sleep for the remaining time
    }
    return 0;
}


After compiling this example, to test the executable, first run mrc-sim

Moving the robot

Now finally, let's connect to the robot (even though it is a simulated one...)! As was already stated, we can use two types of inputs from the robot:

  • The laser data from the laser range finder
  • The odometry data from the wheels

And, in fact, we only have to provide one output:

- Base velocity command

That's it! All we have to do is create a mapping from these inputs to this output! We provide an easy to use IO object (IO stands for input/output) that can be used to access the robot's laser data and odometry, and send commands to the base. Let's take a look at an example:

#include <emc/io.h>
#include <emc/rate.h>

int main()
{
    // Create IO object, which will initialize the io layer
    emc::IO io;
    // Create Rate object, which will help keeping the loop at a fixed frequency
    emc::Rate r(10);
    // Loop while we are properly connected
    while(io.ok())
    {
        // Send a reference to the base controller (vx, vy, vtheta)
        io.sendBaseReference(0.1, 0, 0);
        // Sleep remaining time
        r.sleep();
    }
    return 0;
}



First, try to understand what is going on. You should already be able to derive what will happen if this program is executed.

Now, note a few things: The IO connection is created by just constructing an emc::IO object, it's that easy! We will loop as long as the connection is OK. Then, we can send a command to the base by simply calling a function on the io object.

We do this at a fixed frequency of 10 Hz.

Now, fire up the simulator and visualization, and run the example. And what do you see? Voila, we move the robot using our application! Try modifying the sendBaseReference arguments, and see how they affect the robot behavior.


Sensor inputs

This section provides a short description of the laser data, odometry data, and bumper data that can be obtained through the IO object introduced earlier.

Laser Data

To obtain the laser data, do the following:


emc::LaserData scan;

if (io.readLaserData(scan))
{
    // ... We got the laser data, now do something useful with it!
}

The LaserData struct is defined as follows:

struct LaserData
{
    double range_min;
    double range_max;
    double angle_min;
    double angle_max;
    double angle_increment;
    std::vector<float> ranges;
    double timestamp;
};

The range_min and range_max values define the smallest and largest measurable distances. Any distance reading outside this range, either below range_min or above range_max, is considered invalid. The angle_min and angle_max values specify the angles of the first and last beams in the measurement. The angle_increment represents the angular difference between consecutive beams. However, this value is redundant, as it can be derived from angle_min, angle_max, and the total number of beams.

The actual sensor readings are stored in ranges, which is an std::vector of float numbers. Each element in this vector corresponds to a distance measurement in meters at a specific angle. The angle for each reading can be calculated using angle_min, angle_increment, and the index of the vector element.

Lastly, the timestamp indicates when the data was recorded. It is represented in Unix time, i.e., the number of seconds since January 1, 1970. While the absolute value of the timestamp is not necessarily important, it can be useful for tracking the laser data over time or synchronizing it with other data sources, such as odometry.

Odometry Data

The robot has a holonomic wheelbase, consisting of two wheels arranged in a differential drive configuration, and a rotational joint that allows the entire body to rotate. This particular wheel configuration allows the robot to move both forward and sideways, as well as rotate around its axis. Each of the wheels and the rotational joint is equipped with an encoder that tracks the rotations of that wheel. By using all three encoders and knowing the specific wheel configuration, we can calculate the robot's displacement and rotation from its initial position. In other words: we can calculate how far the robot drove and how far it rotated since it's initial position. This process of calculating translation and rotation based on the wheel encoders is known as odometry.

However, it's important to note that odometry is prone to drift. This means small errors from measurement inaccuracies and wheel slippage can accumulate over time, making it unreliable over longer periods. As such, it's not recommended to rely solely on odometry data for long-term navigation. Additionally, keep in mind that odometry data doesn't start at coordinates (0,0).

To obtain the odometry information, do the following:

emc::OdometryData odom;

if (io.readOdometryData(odom))
{
    // ... We got the odom data, now do something useful with it!
}

The OdometryData struct is defined as follows:


struct OdometryData
{
    double x;
    double y;
    double a;
    double timestamp;
};

Here x , y and a define the displacement and rotation of the robot since the previous measurment, according to the wheel rotations. The translation (x, y) is in meters. The rotation, a is in radians between -pi and pi. Like the laser data, the odometry data also contains a timestamp which is in seconds (Unix time).