0

Line Robot - Advanced concepts

Overview

Here we will revisit some more difficult concepts mentioned in earlier lessons, giving a little broader explanations of the background, but we will not go into details. If You are a beginner, this text will probably not be appropriate for You. There are various topics mixed here but You do not have to program anything so we hope it will not be a problem.

Program structure

Open header file C:\Users\[Your login]\Documents\Arduino\libraries\mrm-robot\src\mrm-robot-line.h. The code has rather ample documentation. Here is a general view.

There are 2 main types of classes.

  • RobotLine. As the name suggests, this class represent the robot.
  • A number of classes derived from ActionBase base class and they represent various actions.

Inheritance and polymorphism

There are different kinds of robots that can be built using ML-R parts. There can be various programs, one for each robot. This would lead to repeated code - important thing to avoid by all means. The solution that is used here is C++ inheritance: an object-oriented concept that is beyond scope of this short introduction. Just a short example, that is used id ML-R robotic code, also in this robot. We have a general concept of robot and some more specific robots: RCJ Rescue Maze robot and RCJ Rescue Line robot. This is "a kind of" relationship. Line robot is "a kind of" robot.

Without further going into details let's see what benefits we can get from this concept. All the general code will be a part of robot's program and we will can this part "Robot class". All the code that is specific for RCJ Rescue Line will form a part called "RobotLine class".

Where will a code that a specific sensor go? It may be used by RobotLine, but some other kinds of robots may use it. We will put it into Robot class. Where will the other code for sensors and effectors (like motors) go? We will put them all into Robot class. Well, most of it. If there is a specific function for some sensor, valid just for RobotLine, this part will go into RobotLine class. CAN Bus handling and many other things will also form Robot class.

The best part is that all that Robot class code doesn't need to be visible for the programmer of the RobotLine and will not clutter its program space. He will only need to change RobotLine code, like line-following algorithm, way the data in the evacuation area are stored, etc. We will have a clear separation of both data and functions, which can be developed and tested independently.

We split the code among different classes, but how we will use it? A general example: let's imagine that You have to develop a program that has to drive a robot along a given trajectory. The robots can have quite different motor groups, for example soccer robots with 4 omni wheels that are positioned in a funny way, tank-like omni-wheel robots, etc. All You know is that they all are derived from the base clase RobotBase and they all implement a common function (go()) for driving the robot along any trajectory needed in Your task. Now comes polymorphism to rescue You. If Your function takes a pointer to RobotBase as an argument, all You have to do is use go() function in Your code. C++ will compile fine and will determine, in runtime, object's (robots) actual derived class and will invoke the correct implementation! So, You will not know which robots will use Your function and how to drive them, but they will still work all fine. If we program some other functions in this way, we will be able to make a program, not knowing which kind of robot will be using it! This is very powerful concept. You cannot do that in C.

Actions as classes

A robot program loops continually. A way to implement this behaviour is to have a main loop. Therefore, Arduino has one. How to program different actions using Arduino loop? In fact, it cannot be done well. You could have a big "switch" statement in the loop, calling each action's function. However, the different functions must use some common data. Function signatures can become quite long and there will always be some common data that will be implemented as global variables - and that is a bad programming design. Not to mention that this loop will get bigger and bigger: a real monster, without a clear separation of data and actions. So, the decision we made is not to use Arduino loop.

Instead we made a loop internal to RobotLine class, which uses other classes (derived from ActionBase) as actions.

Actions serve a few purposes.

  • They encapsulate in classes actions robot has to perform. So, we have classes for robot's parts, but here also for non-material terms.
  • No global variables are used. When an information should be shared between one (but called repeatedly) or more functions, it will be stored inside the action object. For example, all the start conditions will be in the object itself.
  • You can use inheritance to indicate relationships between actions, which indeed exist. For example, a movement can be movement straight ahead or turning.
  • You can use in a consistent way actions defined for the base robot, without its code being exposed here.
  • The actions are included in menus just by including a parameter in the constructor call.
  • Buttons can be used to start actions, as well as menu commands. Menus are displayed both in the connected PC and a Bluetooth device, like a mobile phone, and any of the 2 can be used to issue commands.

Frequency of the main loop

Program flow in a robot's program consists of a loop, that is constantly run and which invokes other functions. The question is: how much time shall these functions consume before returning to the main loop? Should they return almost immediately or there is no such a pressure? In other words, should the functions contain time consuming local loops?

Well, it is definitely easier to allow them to have some. For example, if a function's task is to turn the robot by 90º, it will surely be easier to have a local loop that compares compass to the target value, before returning to the main loop. Obviously, this is a profligate local loop, depriving the main loop of its higher frequency. Without that local loop, the main loop will have to call the function many times, each time checking if the target condition is met and this is harder to do. The function will be more complicated as it should do some preparing work in the first run (like setting the target angle value), that mustn't be done in following runs. So, it has to know if it is a first run or not. There are other problems, too.

There are some disadvantages of the described local-loop strategy. The frequency of the main loop is radically decreased, below 1 Hz. That is way too low for some work that has to be done regularly, like exchanging CAN Bus messages, blinking LEDs, or checking for some other events. True, some of there actions can be done using timer interrupts, but this may be awkward and complicates programming logic and debugging, as the program flow is no more deterministic, but rather jumping from one part into another. Interrupts also burden the MCU and can, in extreme cases, choke the program.

In our opinion, it is better to have a higher main loop frequency and we will show this approach. Action classes will mitigate problematic parts as the can contain data which can be used in different parts of program.

Program's start

Entry point is imposed by Arduino's design: function setup() in MRMS_ESP32.ino:

Robot *robot;

void setup() {
	robot = new RobotLine(); // RobotLine, RobotSoccer, or Your custom robot
	robot->run();
}
robot is declared as a pointer to the base class, Robot. Crucial line is the one containing new operator, which defines it as a pointer to Robot's derived class, RobotLine. If we changed the line to "new RobotMaze()", the program flow would take a quite different route, starting robot for RCJ Rescue Maze. The next line starts the robot. Note that this function has to be implemented in RobotLine as well as in RobotMaze. What it will do, it depends on the line before. At this point we can say goodbye to .ino because the program will never return here. Let's see where it landed in RobotLine (or Robot) class.

Do not look for run() in RobotLine - You will not find it there. It resides in Robot.cpp (and header Robot.h). You can check how it looks like but we will not consider its details here. In short, run() is the main loop and it manages Action objects, therefore distributing workflow. It is not normally advisable to change this function or any other in Robot class. For You it is only important to know how to start the actions You need.

Motors

For driving the motors, we used "motorGroup". You could make Your program for that task, but it is easier to use the mentioned "motorGroup". Let's examine what "motorGroup" is.

Find RobotLine::RobotLine() (constructor) in mrm-robot-line.cpp and the lines we described earlier:

RobotLine::RobotLine() : Robot() {
	motorGroup = new MotorGroupDifferential(mrm_mot4x3_6can, 0, mrm_mot4x3_6can, 2, mrm_mot4x3_6can, 1, mrm_mot4x3_6can, 3);
...
This line defines motor controller for every wheel. Here we use the same one for all of them (mrm_mot4x3_6can). The 4 numbers are motor outputs of the controller.

Motor group

Without studying the details, we can say that motorGroup will be an object of class MotorGroupDifferential. To drive the robot, You can forget about this construction and just remember that go() will start the motors. It is not necessary, but stop() may also be handy. You experienced the same situation with Arduino's standard Serial object if You ever used Arduino sketches. You know what Serial.print() does and probably never bothered too much with Serial itself. In our case, we will nevertheless spend a few sentences just to get acquainted with our class. At the same time we will try to convince You a little more that object oriented programming is cool.

One way to use the motors is to start from scratch. ML-R motor controllers have defined CAN Bus interfaces where You will find commands, for example here. Use setSpeed() function and that's it. The motor is spinning. So, problem solved, why reinvent the wheel? Because the problem has not been solved. Your objective is to drive a robot, not to spin one motor, and that can be more complicated. You must coordinate a couple of motors, maybe use encoders, turn the robot (by coordinating the motors), use PID controller, drive omni-wheels, etc. Just try to move a soccer robot or try to manage maximum acceleration. Even the last issue, acceleration, cannot be solved for each motor separately, but only for the whole group. And so on. In the long run, You will spend much less time by using a ready-made class MotorGroup.

As with ActionBase, this class has a base class, MotorGroup, and derived classes: MotorGroupDifferential (tank-like motion) and MotorGroupStar (soccer-robot-like motion). Like You already did with RobotLine, You can choose the class You need in the instruction above (motorGroup = new...).

PID controller

We established proportional dependance between a positional error and robot's motion in the examples. However, there are some more problems. For a longer explanation, check this article covering PID controller . In short, let's say that the robot follows a wall at 100 mm and it is 110 mm away from it. There is a positional error of 10 mm and our algorithm will instruct the robot to turn slightly towards the wall. Let's imagine further that the robot's current direction is sharply towards the wall already. Turning it even more towards the wall will do no good. This will start an oscillatory movement. In this example, correcting error in position will not be enough. Instead, we will have to consider the rate of change of the error. Therefore, its first derivation. You can develop Your own algorithm for proportional, integral, and integral dependencies. There is a simple ML-R PID controller's implementation. Note that this example is not peculiar but that this type of error correction is a must for most of the models. As the real world values do not stop changing abruptly, they will overshoot the target value, be it temperature, body motion, or something else. More about this problem later.

ActionBase actions

So, how to start the program logic that will drive the robot through the arena? We mentioned ActionBase class before. Let's investigate how it can do some useful work for us. Unfortunately, each ActionBase's derived class has to be programmed in 6 different places. This is C++ mandatory program structure. But, once we do it, it will be easier for us in the long run.

Our action is called ActionRCJLine. It is easy to find all the 6 places by searching code with Ctrl-F.

mrm-robot-line.h

First, class forward declaration, near the beginning of mrm-robot-line.h:

/* All the Action-classes have to be forward declared here (before RobotLine) as RobotLine declaration uses them. The other option would be
not to declare them here, but in that case Action-objects in RobotLine will have to be declared as ActionBase class, forcing downcast later in code, if
derived functions are used.*/
...
class ActionObstacleAvoid;
class ActionLineFollow;
class ActionRCJLine;
You can see this declaration in the last line, after other ActionBase derived classes.

A little lower, in the same file, You will find the following block:

	// Actions' declarations
...
	ActionLineFollow* actionLineFollow;
	ActionRCJLine* actionRCJLine;
...
Here, a pointer is declared. This a way to form "has" relationship between RobotLine and ActionRCJLine: RobotLine "has" ActionRCJLine. We will be using this pointer as an exclusive way to get to this action.

Next, actual declaration:

/** Start RCJ Line run.
*/
class ActionRCJLine : public ActionBase {
	void perform() { ((RobotLine*)_robot)->rcjLine(); }
public:
	ActionRCJLine(Robot* robot) : ActionBase(robot, "lin", "RCJ Line", 1) {}
};
In this declaration we even defined perform() function and constructor so that we are not forced to split the code to even more locations. If You compare this code with other actions, You will see that they are very similar, differing only in perform(), where our code calls rcjLine(), and some base constructor's arguments: "lin" and "RCJ Line". Look at the comments before all actions in code to find explanations. The 2 strings define what will be displayed in menu and rcjLine() is the actual action, function that implements this behaviour. When we define this function, our work will be done.

The last part in mrm-robot-line.h is the declaration of the rcjLine() function, the one action uses to do its task:

	/** Starts the RCJ Line run after this action selected.
	*/
	void rcjLine();

mrm-robot-line.cpp

Let's move to mrm-rob-line.cpp, where the rest of code is. Find this block:

	// All the actions will be defined here; the objects will be created.
...
	actionObstacleAvoid = new ActionObstacleAvoid(this);
	actionWallFollow = new ActionWallFollow(this);
It resides in RobotLine's constructor, the part that is executed right during the creation of the RobotLine object.

Finally, we have to implement rcjLine() function:

/** Starts the RCJ Line run after this action selected.
*/
void RobotLine::rcjLine() {
...
}

How to start an action?

Robot's useful action usually includes some kind of moving around. How to start it? The easiest way is to start it always, as soon as robot is powered on. This solution is highly inadvisable. If You wonder why, wait till it hits the floor, after jumping from the desk.

Way to do it in ML-R is by using ActionBase derived classes, which were described on previous page. They offer You 3 possibilities.

Program call

First is to start it in code, like this:

actionSet(actionLineFollow);
This line is a part of rcjLine() function in mrm-robot-line.cpp and is used to initiate line-following action. Therefore, the action can be started by calling actionSet() function and including action object's name as the only parameter. First time the program flow passes through main loop, it will replace the current action, causing ActionLineFollow perform() function to start executing repeatedly, as long as ActionLineFollow stays the current action. You can check in ActionLineFollow declaration that perform() calls RobotLinee::lineFollow() function, being the one that actually contains the working code.

Menu

A menu is displayed each time robot enters idle state (ends actions), both in USB connected computer, and on a paired Bluetooth device, like a mobile phone or tablet. You can use any of these devices to issue commands in form of 3-letter shortcuts, which are again defined in action's constructor definition:

/** Start RCJ Line run.
*/
class ActionRCJLine : public ActionBase {
	void perform() { ((RobotLine*)_robot)->rcjLine(); }
public:
	ActionRCJLine(Robot* robot) : ActionBase(robot, "lin", "RCJ Line", 1) {}
};
According to this definition "lin - RCJ Line" will be displayed in the main menu (parameter "1" states that) and the action can be started by typing "lin" (depending on terminal's implementation, additional "Enter" or "Send" button may be necessary).

Button

A robot featuring ML-R 8x8 bicolor display, CAN Bus,UART, 4 switches (mrm-8x8a) has 4 user defined buttons. Even the main microcontroller, MRMS ESP32: Arduino, IMU, eFuse, BT, WiFi, CAN Bus (mrm-esp32) has one, although not so easily accessible. To use a button, it has to be associated with the desired action. To link it to RobotLine, use this line, like in the MRMS_ESP32.ino program:

	// Set buttons' actions.
	mrm_8x8a->actionSet(actionRCJLine, 0); // Button 0 starts RCJ Line.
This line is a part of RoboLinee's constructor. You can add Your actions after this one.

Action end

Ending an action is easy in 2 ways:

  • by setting another action, using actionSet() function or
  • by calling end() function.
After action end, the robot will enter idle state, which does nothing except first displaying a menu and afterwards waiting for another user input (button or keystrokes).