5 An example robot program

Having now got to grips with the RoboLab environment, it’s time to start looking at what sort of robot control programs we can actually run in the simulator.

In this notebook you will see some example robot control programs in action. At this stage, you do not need to know how to create such programs yourself: the aim of this week’s activities is mainly to familiarise you with RoboLab and the sorts of things we can get the robot to do in the simulator. You will also start to get a feel for the length and complexity of the programs you will be writing for yourself by the end of the robotics block practical activities.

In next week’s activities, we go back to basics with a proper introduction to Python programming, before embarking on some simple robot control programs written using Python code.

So let’s get started and see what sort of things we can get the simulated robot to do…

Remember, you need two things for the %%sim_magic to work:

  • first, the magic must be loaded by running the %load_ext nbev3devsim command in a code cell

  • second, a code cell must start with %%sim_ETC at the start of a new code cell, followed by the code you want to download starting on a new line in the code cell. If there is a new line or white space or other text before the %%sim_ETC in the code cell, then it won’t work.

5.1 Keeping the robot within an area

Let’s start with an example that shows how to keep a robot inside a particular area bounded by a boxed area marked out on the floor of the world.

You are not expected to understand the program code used to control the robot at this point, although by the end of this block you should be able to write programs of this sort by yourself.

Rather, the aim here is simply to demonstrate some of the features of the simulator and some of the approaches we will be using to try to simplify ways of including ‘boilerplate’ code (that is, particular code elements and definitions we need to include in most of our programs in order write our robot control programs).

Load the simulator package and then load and display the simulator widget:

from nbev3devsim.load_nbev3devwidget import roboSim, eds

%load_ext nbev3devsim

Select the Loop background which loads the robot in to the centre of a large rectangle drawn with thick black lines, either from the user interface or via a line magic command:

%sim_magic --background Loop

The following ‘stay inside’ program causes the robot to move forwards until its light sensor detects the black contour, at which point the robot reverses direction. When it encounters the contour again it changes direction. In this way the robot shuttles backwards and forwards inside the contour indefinitely.

Run the code cell to download the program to the simulator and resize the widget so you can see the loop background in the simulator.

Click on the simulator Run button, or use the simulator keyboard shortcut (R) to run the program in the simulator. When you are ready to stop the program, click on the simulator Stop button.

%%sim_magic_preloaded --background Loop -O

# Program to stay inside a bounded area

# Start to drive forwards
tank_drive.on(SpeedPercent(50), SpeedPercent(50))

# Set up a loop to run the set commands
# over and over again
while True:
    # Save the light sensor reading
    light_level = colorLeft.reflected_light_intensity_pc
    
    # Display the light sensor value
    # in the simulator Output display window
    print('Light_left: ' + str(light_level))

    if light_level < 40:
        # Drive backwards a set distance 
        tank_drive.on_for_rotations(SpeedPercent(-50),
                                    SpeedPercent(-50),
                                    2)

        # Drive in a turn for 2 rotations
        # of the outer motor
        tank_turn.on_for_rotations(-100,
                                   SpeedPercent(75),
                                   2)

        # Drive forwards again
        tank_drive.on(SpeedPercent(50),
                      SpeedPercent(50))
        
        # Last line of if conditional block
        
    # Last line of while loop block

# Program ends

5.2 Anatomy of the RoboLab ‘stay inside’ program

Having run the ‘stay inside’ program, and observed the behaviour of the robot, let’s now read through the program to see how it works.

If you click in the previous code cell to select it:

  • you can toggle the display of line numbers on and off by repeatedly using the keyboard shortcut Esc-L

  • you can ‘tear off’ the code cell into its own floating widget that you can then refer to by clicking the >_ notebook toolbar button; click the x button in the top right-hand corner of the widget to return the cell to the body of the notebook.

At the start of the program (line 1) is the cell block IPython magic which is prefixed by two (%%) signs. The %% prefixed magic is a cell magic that is used to modify the behaviour of the contents of the whole cell.

The magic we are using is the ‘preloaded’ magic with the --background switch set to load in the Loop background and the -O switch to display the Output display panel:

%%sim_magic_preloaded --background Loop -O

Blank lines, such as lines 2 and 4, have no effect and are used to help make the program more readable.

The next two lines (lines 3 and 5) are comments:

# Program to stay inside a bounded area

# Start to drive forwards

For these lines, a # (‘hash’ or, in American English, a ‘pound’) sign identifies the line as a comment line; in this case, the first line gives a very concise statement of the objective of the program, the second describes what an actual line of code is intended to do.

Comments are ‘free text’ areas that are not executed as lines of Python code. As such, they can be used to provide annotations or explanations of particular parts of the program, or to ‘comment out’ lines of code that are unnecessary.

The control program properly starts by using a ‘tank drive’ command in line 6 to drive the robot forwards at about half its full speed (the left wheel and the right wheel are both powered on at 50% of their maximum speed).

tank_drive.on(SpeedPercent(50), SpeedPercent(50))

Another comment, split over lines 8 and 9, prefaces the while True: loop statement in line 10:

# Set up a loop to run the set commands
# over and over again
while True:

The while True: command means repeat everything inside the indented code block below the while True: command over and over again for ever (or until the user stops the program).

The : is required and it defines the entry point into the indented code block if the condition tested by the while statement evaluates as True.

The next line, line 11, is an indented comment line, and starts the definition of a code block, each line of which will be executed in turn. The lines of code that define the code block, lines 11 to 36, are indented to the same level as the first line of the code block. (Some lines are then followed by their own, further indented, code blocks.) If the condition evaluated by the while statement was not true, then the code in the indented code block would not be executed.

Line 12 then defines a named variable, light_level, that takes the current numerical value of the colorLeft.reflected_light_intensity_pc reflected light sensor reading.

    # Save the light sensor reading
    light_level = colorLeft.reflected_light_intensity_pc

Lines 14 and 15 are comment lines, followed by a print() statement in line 16 that prints the sampled light sensor value as a string to the Output display panel:

    # Display the light sensor value
    # in the simulator Output display window
    print('Light_left: ' + str(light_level))

As you will see later, this value can also be viewed via a dynamically updated chart, as well as analysed ‘offline’ in the Python notebook when the simulation run has finished.

The next line in the code block, line 18, is a conditional if statement that checks to see if the sampled light sensor reading (light_level) is less than (<) a specified value (40) and sets up the entry point to a code block (:):

    if light_level < 40:

Following the if statement, we start another new code block, with another level of indentation, extending over lines 19 to 34.

If the robot is over the black line, the recorded value for the reflected light value is less than the threshold value and the code in this code block is executed. If the robot is over the white line, a value higher than the threshold is recorded, and on this iteration of the while loop the code in the if block is ignored.

The first line of code in the conditional if block, line 19, is a comment that describes the action of the command split over lines 20 to 22 that drives the robot backwards at half speed for 2 rotations of the wheels:

        # Drive backwards a set distance 
        tank_drive.on_for_rotations(SpeedPercent(-50),
                                    SpeedPercent(-50),
                                    2)

(According to the documentation, if the left speed is not equal to the right speed (i.e., the robot will turn), the motor on the outside of the turn will rotate for the full rotations while the motor on the inside will have its requested distance calculated according to the expected turn.)

After the robot moves backwards, the comment on lines 24 and 25 describes the action of the command split over lines 26 to 28, which turns the robot on the spot by a specified amount:

        # Drive in a turn for 2 rotations
        # of the outer motor
        tank_turn.on_for_rotations(-100,
                                   SpeedPercent(75),
                                   2)

Lines 30 to 32 describe and carry out the action of putting the robot into a ‘free running’ forward driving motion again.

        # Drive forwards again
        tank_drive.on(SpeedPercent(50),
                      SpeedPercent(50))

Line 34 is a comment that informally closes the if block (this is not required), and the comment in line 36 optionally and informally announces the end of the while block. Finally, line 38 informally and optionally identifies the end of the program.

        # Last line of if conditional block
        
    # Last line of while loop block

# Program ends

The control flow initiated by the while loop causes the code within the while block to be repeatedly executed. On each iteration of the loop, the light sensor value is polled (sampled) and is tested against the threshold value. If the sensor value is less than the threshold, then the code in the if block is executed, but not otherwise. This means that the robot just drives forwards unless it ‘sees’ the black line, at which point it reverses the direction of the motors, and then turns before driving forwards again. In this way the simulated robot shuttles backwards and forwards, staying inside the area defined by the contour.

If you ‘popped out’ the code cell to follow along the reading of program, close the widget it was popped out into now to return the cell to the main body of the notebook.

5.2.1 Activity – Simulated versus real-world physical robots

How do you think the way the simulated robot behaved in this activity might compare with the actions of a real robot operating in the physical world?

Double-click this cell to edit it and enter your thoughts here, then ‘Run’ this cell to return it to the styled / rendered HTML view.

Example discussion

Click the arrow in the sidebar or run this cell to reveal the example discussion.

A simulated robot operates in a purely computational environment and as such operates in a ‘pure’ numerically defined environment.

A real robot may not shuttle backwards and forwards as precisely as the simulated robot does. Real robots are ‘noisy’, but not just in terms of the sound they make. There is also ‘noise’ in their mechanical gearing and control: the motors don’t go at precisely the expected speed, the gears may not mesh perfectly, the wheels may slip or skid, and the sensors do not give instantaneous or perfect readings. All of these mechanical forms of noise may cause the robot to not drive in a perfectly straight line, or it may turn by a slightly different amount on each turn.

The light sensor readings may also be subject to noise. For example, vibration or bumps in the surface may cause the light sensor to bump up or down slightly, changing the height of the light sensor above the surface and changing the amount of reflected light. Shadows or changes in illumination over the extent of the background may also cause false positive identifications of areas cast in shadow as black lines.

In other words, real robots may have sloppy and relatively unpredictable mechanisms so that the same control commands from the same initial position may result in a variety of outcomes. (For this reason, the RoboLab simulator has a noise feature that you will meet later that allows you to set random variations to the motor speeds and sensor readings. This ‘noise’ makes the simulated robot behave more realistically. It will be used in later RoboLab sessions.)

5.3 The complete ‘stay inside’ program

In the previous program, the tank_drive and tank_turn elements were predefined by our use of the %%sim_magic_preloaded magic. This really is a bit like magic, because it allows us to write Python code that would not ordinarily be valid code. In the background, the magic itself defines some essential Python code that is prepended (that is, added to the start of) our program code before it is downloaded to the simulated robot.

We can see what code is prepended to the code provided in the code cell before it is downloaded to the simulator using the magic as a line magic with the --preview switch:

%sim_magic_preloaded --preview

You can also view the full program downloaded to the simulator in the simulator Code display:

%sim_magic -WH --code

5.4 Using less magic…

The following code cell uses a slightly less powerful magic, %%sim_magic_imports, that still masks some of the complexity in creating a valid Python program although in this case it does require you to define the tank_turn and tank_move statements in terms of slightly lower-level building blocks. The program in the code cell is intended simply to drive the robot forwards a short way and then stop.

If you download the program and autorun it, then you will hear an error tone and an error message will be displayed in the Output window with a reference to the line number in the complete downloaded program where the error occurred.

You can cross reference the line at fault by looking at the listing in the simulator to see which line in the prepended, downloaded program was at fault.

Note that the corresponding line number in the code cell is not the same line number in the full, downloaded program.

%%sim_magic_imports --background Empty_Map -OWHDR

# Start to drive forwards
tank_drive.on_for_rotations(SpeedPercent(50),
                            SpeedPercent(50),
                            4)

If we look at the error, we see it attempts to identify what is at fault and where:

NameError: name 'tank_drive' is not defined on line 14

Note that program execution stops at the first encountered error. If there were other errors later on in the program, they would not be reported until the program could run through all the previous lines of code without error before encountering the erroneous line.

If we look at the code prepended by the sim_magic_imports magic, we see that we have not declared tank_drive.

%sim_magic_imports --preview

If we check the code prepended by the %sim_magic_preloaded --preview magic, we see that the tank controls were defined in that case, along with some predefined sensor elements and some other bits of code:

%sim_magic_preloaded --preview

5.4.1 Fixing the broken program

To fix the program, we can either revert to the original %%sim_magic_preloaded magic or we could ensure that any missing declarations are provided.

The following program should now run correctly:

%%sim_magic_imports --background Empty_Map -HR

# Necessary tank_drive definition
tank_drive = MoveTank(OUTPUT_B, OUTPUT_C)

# Start to drive forwards
tank_drive.on_for_rotations(SpeedPercent(50),
                            SpeedPercent(50),
                            4)

5.4.2 Optional activity – Diffing the prepended code strings

Click the arrow in the sidebar or run this cell to reveal the optional activity.

In many programming environments, a wide range of tools are provided to support code development and debugging. One such tool is known as a ‘differ’ or ‘differencer’ that can be used to highlight differences between two separate files or strings.

Typically, additions in the second file compared to the first are highlighted in green, and deletions from the first file compared to the second are highlighted in red or pink.

In the following code cell, the --previewcode switch is applied to the %sim_magic_preloaded and %sim_imports magics to retrieve the code that each of them prepends. These two strings are then compared and the difference between them displayed.

from nbcelldiff import diff_match_patch
from IPython.display import HTML

# Create a differencer object
differ = diff_match_patch.diff_match_patch()

# Load in the code strings appended by the magics
magic_preloaded = %sim_magic_preloaded --previewcode
magic_imports = %sim_magic_imports --previewcode

# Compare the strings and display the difference
diff = differ.diff_main(magic_imports, magic_preloaded)
display(HTML('<br/><code>'+differ.diff_prettyHtml(diff)+'</code><br/>'))

5.5 Using the littlest simulator magic: a complete Python program

The following code cell shows a fully defined Python program. In this case, a series of ‘package import’ statements appear at the start of the program. Python packages are code libraries written to support particular activities.

The core Python language includes a variety of packages that are distributed as part of the Python language, but additional packages can be written using core Python language elements, or Python commands imported from other additional packages, to build ever more powerful commands.

In particular, the Python ev3dev2 package provides a range of language constructs that allow us to write a Python program that can work with our nbev3devsim simulated robot.

The same program should also be runnable on a real Lego EV3 brick running the ev3dev operating system.

As you can see from the code cell below, which uses the minimal %%sim_magic magic to download just the contents of the cell to the simulator, in this case we explicitly import the custom elements we need from the ev3dev Python package that we then make use of in our program.

%%sim_magic --background Empty_Map -HR

# Import required declarations 
# from the ev3dev2.motor package
from ev3dev2.motor import OUTPUT_B, OUTPUT_C
from ev3dev2.motor import MoveTank, SpeedPercent

# Necessary tank_drive definition
tank_drive = MoveTank(OUTPUT_B, OUTPUT_C)

# Start to drive forwards
tank_drive.on_for_rotations(SpeedPercent(50),
                            SpeedPercent(50),
                            4)

5.6 Using the magics to simplify our programs

Rather than having to explicitly load in various elements from the ev3dev2 Python and explicitly define configured motor and sensor elements again and again using boilerplate code (that is, standard code that is the same every time we need to use it), the %%sim_imports and %%sim_preloaded magics will prepend our programs with necessary boilerplate code on our behalf, simplifying them considerably.

However, you should note that as such it means we can write programs in code cells that are not, in and of themselves, strictly well defined: if we copied and pasted the code directly into the simulator, the program would be lacking essential definitions and would fail to run.

5.7 Robots that speak

With all that talk of errors and failure, let’s finish this notebook on a more upbeat tone by looking at one more element you may have noticed being preloaded by the %%sim_magic_preloaded magic: the say() function.

As well as moving about the simulated world, the robot can also affect the state of the world by making a noise in it.

Run the following code cell to download the program to the simulator and autorun it to hear what it has to say for itself…

%%sim_magic_preloaded -HWR
say("Hello. How are you doing today?")

See if you can modify the name string so that the robot says ‘TM129’. Can you get it to say something that sounds more like tee, em, one, two, nine rather than one hundred and twenty nine?

5.8 Summary

This notebook started with a demonstration of the sort of behaviour we can program our simulated RoboLab robot to perform inside the simulator – specifically, moving around and staying inside a box. You then saw, line by line, how the program was constructed to perform the task, including: the use of cell magic at the start of the cell to identify the code that followed as code to be downloaded to the simulator; the use of comments in the program code; the use of nested code blocks within loop and conditional statements; and the use of print statements that could report messages to the simulator Output display window.

The simulator itself also uses the Output display window to alert us to errors within our program when we try to run it. In many cases, errors are identified with an associated line number. Using the simulator Code display, you can preview the whole program downloaded to the simulator, not just the explicitly written code in a magicked code cell, along with line numbers to help find the erroneous code line in the context of the complete program around it.

Finally, you also saw how we can use the simulator magics to prepend different amounts of standard boilerplate code to our program before downloading it to the simulator. In particular, the boilerplate code could import several essential packages as well as preconfiguring the motor drives and sensor variables on our behalf and providing us with a means for letting the robot speak for itself.

This completes the practical activities for this week.