Beyond Basics Ex 1-6

To see how each of these exercises can be solved in the standard Lego EV3 programming environment (EV3-G) click HERE.

Exercise 1: Multitasking (multithreading)

What some languages call 'multitasking' is called 'multithreading' in Python. The interest of the exercise is of course that it demonstrates multitasking i.e. the running of multiple branches or tasks or 'threads' simultaneously. A thread is a part of program code that can run independently and at the same time as other parts of the program. Unlike EV3-G, Python does not have a command that can be run in one thread to instantly stop another thread running. Multithreading in EV3 Python is more awkward than it is in EV3-G so you may want to seek alternative solutions that do not involve multithreading - this is often possible.

In the script below the play_sound() function definition contains a loop that stops running when the loop variable is set to False, which happens in the last line of the main code block.

You should know about the important concept of 'scope' already - this site won't teach it to you. If you know about scope you may wonder why it's not necessary to make loop 'global' in the play_sound() function definition. Without 'global loop', shouldn't the function create a local variable (with local scope) that is not the same as the loop variable in the main code block? And shouldn't that mean that when loop is set to False in the main code block it won't affect the independent loopvariable in the function definition? In fact the function uses the same loop variable that is in the main code block because the variable 'loop' is created in the main code block before the play_sound() function is called. Making variables global should generally be avoided if possible - check the special multithreading page for alternative methods.

Objective:

The robot will continuously play the Lego 'Motor idle' sound until the robot has moved straight forward for two wheel rotations at 50% of the rated maximum speed (speed=50). The Lego EV3 software includes a collection of sounds in Lego's own RSF sound file format. EV3 Python cannot play sounds in that format but it can play sounds in WAV format. You can find the same sounds in WAV format at the bottom of the 'Sound' page. All the sounds are in a single folder, unlike the sounds used by EV3-G. For compatibility with the scripts in this site you should place all the WAV sound files in a folder called 'sounds' inside the 'robot' folder on the EV3dev SD card. This is very easy if you are using VS Code with the EV3 extension - see the Sound page.

Solution:

#!/usr/bin/env python3

from ev3dev2.motor import MoveSteering, OUTPUT_B, OUTPUT_C

from ev3dev2.sound import Sound

from threading import Thread

steer_pair = MoveSteering(OUTPUT_B, OUTPUT_C)

sound = Sound()

def play_sound():

    while loop==True: # repeat until 'loop' is set to False in the main thread.

        sound.play_file('/home/robot/sounds/Motor idle.wav')

    

loop = True

# Start the play_sound thread.

t = Thread(target=play_sound)

t.start()

steer_pair.on_for_rotations(steering=0, speed=50, rotations=2)

loop = False # set 'loop' to False so that the loop in the play_sound thread

# stops repeating

Notes:

Exercise 2: Loop

Objective:

The program first makes the brick's status light glow red for two seconds without pulsing, then the robot beeps twice and moves forwards one wheel rotation, repeatedly beeping and moving until the touch sensor is pressed. This script uses beeps rather than clicks because beeps (and tones) are built into the EV3 Python whereas clicks are not.

Solution:

This is perhaps the simplest solution, but it's not very faithful to the EV3-G solution and it requires that you stop the program with a LONG press on the touch sensor since it does not continually monitor the touch sensor for presses.

#!/usr/bin/env python3

from ev3dev2.motor import OUTPUT_B, OUTPUT_C, MoveSteering 

from ev3dev2.sensor.lego import TouchSensor

from ev3dev2.led import Leds

from ev3dev2.sound import Sound

from time import sleep

steer_pair = MoveSteering(OUTPUT_B, OUTPUT_C)

ts = TouchSensor()

leds = Leds()

sound = Sound()

leds.all_off() # Turn all LEDs off and also turns off the flashing.

# Set both pairs of LEDs to red for 2s then green

leds.set_color('LEFT', 'RED')

leds.set_color('RIGHT', 'RED')

sleep(2)

leds.set_color('LEFT', 'GREEN')

leds.set_color('RIGHT', 'GREEN')

while not ts.is_pressed: # Stop the program with a LONG press

    sound.beep()

    sound.beep()

    steer_pair.on_for_rotations(steering=0, speed=50, rotations=2)

The above script doesn't use multithreading like the EV3-G solution and therefore won't react instantly to the press of the touch sensor - you need to do a long press. See the multithreading page for a discussion of multithreading and then check out the script below. As you read on the multithreading page, a daemon thread will stop running as soon as the main thread stops. In the script below, the main thread waits for the touch sensor to be bumped and then terminates, causing the daemon thread to also terminate. Changes relative to the script above are in bold.

#!/usr/bin/env python3

from ev3dev2.motor import OUTPUT_B, OUTPUT_C, MoveSteering 

from ev3dev2.sensor.lego import TouchSensor

from ev3dev2.led import Leds

from ev3dev2.sound import Sound

from time import sleep

from threading import Thread

steer_pair = MoveSteering(OUTPUT_B, OUTPUT_C)

ts = TouchSensor()

leds = Leds()

sound = Sound()

leds.all_off() # Turn all LEDs off. This also turns off the flashing.

# Set both pairs of LEDs to red

leds.set_color('LEFT', 'RED')

leds.set_color('RIGHT', 'RED')

sleep(2)

leds.set_color('LEFT', 'GREEN')

leds.set_color('RIGHT', 'GREEN')

def daemon_thread():

    while True:

        sound.beep()

        sound.beep()

        steer_pair.on_for_rotations(steering=0, speed=50, rotations=2)

t = Thread(target=daemon_thread)

t.setDaemon(True) # make the thread called 't' into a daemon thread

t.start()

ts.wait_for_bump()

Exercise 3: IF structure (called 'Switch' in the Lego software)

Objective:

This will be a simple 'line follower' program using a single color sensor. The intention is that the robot will follow a black line that has been drawn on a white mat. This is a little difficult to do using only a single color sensor. Imagine that the robot is moving along the line with the sensor over the black line, then the sensor will detect very little light being reflected. If the robot wanders off the line then the sensor will detect a strong reflected light intensity, but how useful is that information? The robot won't know whether it has wandered off the left side of the line or the right, so it won't know how to turn back towards the line. So the trick is not to try to follow 'the line', it is to follow one edge of the black line, let's say the right edge. Now if the sensor detects that it is over white then we know the robot should turn left to find the edge again and if the sensor detects that it is over black then the robot should turn right to find the edge again. In other words...

Solution:

#!/usr/bin/env python3

from ev3dev2.motor import MoveTank, OUTPUT_B, OUTPUT_C

from ev3dev2.sensor.lego import ColorSensor

from ev3dev2.button import Button

from time import sleep

btn = Button() # we will use any button to stop script

tank = MoveTank(OUTPUT_B, OUTPUT_C)

# Connect an EV3 color sensor to any sensor port.

cl = ColorSensor()

while not btn.any():    # exit loop when any button pressed

    # if we are over the black line (weak reflection)

    if cl.reflected_light_intensity<30:   

        # medium turn right

        tank.on(left_speed=50, right_speed=0)

    

    else:   # strong reflection (>=30) so over white surface

        # medium turn left

        tank.on(left_speed=0, right_speed=50)

    

    sleep(0.1) # wait for 0.1 seconds

Notes:

Exercise 4: Multiple Switch

Note: this exercise assumes that the color sensor on port 3 is aligned horizontally or pointing upwards so you can present differently colored sides of the 'multi-color cuboid' to it.

Objective:

Solution:

#!/usr/bin/env python3

from ev3dev2.motor import MoveTank, OUTPUT_B, OUTPUT_C

from ev3dev2.sensor.lego import ColorSensor

from ev3dev2.button import Button

from time import sleep

btn = Button() # will use any button to stop script

cl = ColorSensor()

tank_pair = MoveTank(OUTPUT_B, OUTPUT_C)

while not btn.any():    # exit loop when any button pressed

    color_str = cl.color_name # Returns an integer, e.g. 'Blue'

    if color_str == 'Yellow':

        tank_pair.on(left_speed=40, right_speed=10)

    elif color_str == 'Blue':

        tank_pair.on(left_speed=10, right_speed=40)

    else:

        # no color detected or a color other than yellow or blue

        tank_pair.on(left_speed=40, right_speed=40)

    sleep(0.01) # Give the CPU a rest

Notes:

Exercise 5: 'Data wires'

In the icon-based Lego EV3 programming environment it is often necessary to use 'data wires' to transfer data between programming blocks. Fortunately, this is not meaningful or necessary in a textual programming environment like EV3 Python.

Objective:

In this program the robot does not move and it is assumed that an object such as your hand will be brought towards the ultrasound sensor on the robot. A loop continuously measures the distance to an object (assumed to be initially more than 8 cm away), and displays the measured distance on the brick's screen. The loop exits when the program detects that the distance to the reflecting object has become less than 8 cm, and an image of two eyes called 'Up' is then displayed for two seconds.

Solution:

This program assumes that have you have downloaded all the standard Lego image files in BMP format to the brick in accordance with the instructions in the 'Display an image file' section on this page. This script uses a font (helvB24).

#!/usr/bin/env python3

from ev3dev2.sensor.lego import UltrasonicSensor

from ev3dev2.display import Display

from time import sleep

from PIL import Image, ImageDraw, ImageFont

lcd = Display()

us = UltrasonicSensor() 

while us.distance_centimeters > 8:

    my_text = str(us.distance_centimeters)+' cm'

    lcd.text_pixels(my_text, x=40, y=50, font='helvB24')

    lcd.update()

    sleep(0.1) # so the display doesn't change too frequently

pic = Image.open('/home/robot/pics/Up.bmp')

lcd.image.paste(pic, (0,0))

lcd.update()

sleep(5)  # need time to admire image

Exercise 6: Random

Objective:

The program will first generate a random integer between -70 and 70 inclusive. The robot should drive forward at this speed for one second, then pause for one second, then generate a new random number and repeat forever.

Solution:

In Python, random integers between a and b inclusive can be generated with randint(a, b). The randint() function is part of the random library so the random library must be imported before the function can be used. We will include code such that the script can be terminated with a long press on any EV3 button.

#!/usr/bin/env python3

from ev3dev2.motor import MoveSteering, OUTPUT_B, OUTPUT_C

from time import sleep

from ev3dev2.button import Button

import random

btn = Button() # press any button to stop the program

steer_pair = MoveSteering(OUTPUT_B, OUTPUT_C)

# exit loop with a long press on any EV3 button

while not btn.any():

    # generate a random integer between -70 and +70 inclusive

    rand=random.randint(-70,70)

    steer_pair.on_for_seconds(steering=0, speed=rand, seconds=1)

    sleep(1)

Notes:

Let's compare this solution with the official EV3-G solution to the same problem. Bear in mind that the official EV3-G solution has no comments.

You are now ready to tackle Beyond Basics exercises 7-11.