Chapter 12. Robots

Robots are cute in an “all humans must die” sort of a way; and, thanks to the educational uses of devices like the micro:bit, many makers have created robots that demonstrate how robotics can be a relatively simple and fun endeavour. This chapter explores two robotics projects that show how to use MicroPython to make your very own robotic invasion. Both use the micro:bit, although the techniques discussed are easy to transfer to other boards running MicroPython.

Trundle Bot

This bot trundles around on wheels. It has an analog distance sensor on the front to detect objects in its way. If something blocks its way, it rotates left or right until no blockage is detected, then it continues on its way. It’s a very simple bot that can be made in about an hour with only a few parts. The code to drive the bot is also beautifully simple and demonstrates how to drive servo motors to give the bot movement.1

The bot was successfully used in a bot-building workshop at EuroPython 2016 and, because of its simplicity, was easy to modify and change to suit the available building materials and aims of the builders (who included experienced Python programmers, their nontechnical partners, and children).

The minimum parts required are inexpensive:

  • A micro:bit
  • 2 9g continuous rotation servos
  • A Pololu Carrier with Sharp GP2Y0A60SZLF Analog Distance Sensor, 3 V
  • 2 wheels
  • A Pololu caster ball
  • A portable power source to provide between 3.3 V and 4.2 V

There also needs to be some means of making a chassis and wires to connect the components together. Double-sided sticky tape, rubber bands, and googly eyes are also helpful for assembling the device. Don’t be too worried about making something that looks as well constructed as Figure 12-1. The point of this robot is to make something that just works. Once it’s working, you can improve the construction. For example, Figures 12-2 and 12-3 show a homemade version of the bot made out of cardboard, sticky-backed plastic, rubber bands, and googly eyes. I’m sure you’ll agree it has a certain charm about it (and more importantly, it’s something a beginner interested in robotics would have fun constructing).

A simple trundlebot
Figure 12-1. A very simple trundlebot
Trundlebot taken apart
Figure 12-2. The trundlebot showing an improvised chassis held together by sticky-backed plastic, twisted wires, tape, and a rubber band
Trundlebot assembled
Figure 12-3. The assembled trundlebot (the micro:bit has sticky backed plastic to hold it in place). Googly eyes make it friendly.

The trick is to use your imagination and have fun. Most importantly, for the robot to work, it needs to be wired up correctly. While Figure 12-4 may at first look complicated, you will soon realise how simple the bot’s construction really is.

Trundlebot wiring diagram
Figure 12-4. The trundlebot wiring diagram

Assuming you have connected the bot and assembled some sort of chassis, the next task is to drive the servo motors to make it move and take readings from the distance sensor so it won’t bump into things.

The servos are physically connected to pins 0 and 1, through which analog signals are sent to control the direction and speed of rotation. The pulse width of the signal is the attribute that enables control. A pulse width of some arbitrary duration corresponds to a stopped motor. A pulse width either smaller or larger than the stopped value causes rotation in one direction or the other. The further away from the stop value the pulse width becomes, the greater the speed or rotation.

This functionality is wrapped up in a Servo class:

import microbit

class Servo:
    def __init__(self, pin, trim=0):
        self.pin = pin
        self.trim = trim
        self.speed = 0
        self.pin.set_analog_period(20)

    def set_speed(self, speed):
        self.pin.write_analog(int(25 + 100 * (90 + speed) / 180 + self.trim))
        self.speed = speed

The class is initialised with a reference to the physical pin used to drive the motor and an argument called trim. Trimming is simply making fine adjustments to something; and, since servo motors are notoriously inconsistent in performance characteristics, there needs to be some way to adjust them. The speed attribute represents how fast the servo is moving, and the configuration is completed by setting the period (frequency) for PWM with the set_analog_period method of the pin. The set_speed method changes the speed of the servo. This can range from -90 (full backwards), via 0 (stop) to 90 (full forwards).

The next piece of the puzzle is representing the robot itself:

class Robot:
    def __init__(self):
        # Remember to check the trim values.
        self.left_servo = Servo(microbit.pin0, 2)
        self.right_servo = Servo(microbit.pin1, 1)

    def go(self, distance):
        microbit.display.show(microbit.Image.ARROW_S)
        self.left_servo.set_speed(-90)
        self.right_servo.set_speed(90)
        microbit.sleep(int(distance * 2000 / 17))
        self.stop()

    def turn(self, angle):
        if angle > 0:
            microbit.display.show(microbit.Image.ARROW_E)
            self.left_servo.set_speed(-90)
            self.right_servo.set_speed(-90)
            microbit.sleep(int(angle * 64 / 9))
        else:
            microbit.display.show(microbit.Image.ARROW_W)
            self.left_servo.set_speed(90)
            self.right_servo.set_speed(90)
            microbit.sleep(int(-angle * 64 / 9))
        self.stop()

    def stop(self):
        microbit.display.show(microbit.Image.DIAMOND)
        self.left_servo.set_speed(0)
        self.right_servo.set_speed(0)

    def get_distance(self):
        return microbit.pin2.read_analog()

This allows us to pull all the components together under the control of a single instance of the Robot class.

Initialisation involves creating two new instances of the Servo class: one for each servo motor. As the comment says, if the robot veers off course when attempting to drive in a straight line, this is where to adjust the trim. The other methods provide all the functionality you need: go, turn, stop and get_distance. The go method uses its distance argument to work out how long to wait while the servo motors are active. Notice how the left and right motors work in opposite directions since they’re mirrored in how they are arranged on the physical device (and thus the direction of travel is reversed). The turn method works in a similar way but given an angle argument, and the stop method does exactly what it says. All three of these methods work by actuating the servo motors in a meaningful way in order to hide the implementation details (at this level of abstraction in the code, we just want developers to think in terms of the robot rather than servo motors). In this spirit, the get_distance method simply returns the analog reading from the distance sensor connected to pin 2. This is an inverse reading: smaller means further away.

Making the robot work requires instantiating the Robot class and driving it from inside a simple event loop:

robot = Robot()
while True:
    robot.go(5)
    if robot.get_distance() > 700:
        robot.turn(20)
        left_distance = robot.get_distance()
        robot.turn(-40)
        right_distance = robot.get_distance()
        robot.turn(20)
        if left_distance < right_distance:
            robot.turn(60)
        else:
            robot.turn(-60)

Whilst in the event loop, the robot is made to go forward. However, if the robot gets a high result from the get_distance call, it follows a very simple algorithm: measure the distance of things to both the right and left of the current position, check which reading has the most available distance to move forward, and then turn in that direction. While this isn’t HAL 9000-level artificial intelligence, it’s certainly enough to keep the bot out of trouble.

The end result is a semi-independent, mobile device that works via a computing “brain”. It is a very simple robot, but that is a good thing because it offers potential and room for improvement.

How might you improve the robot’s behaviour? Could you make it follow objects instead of just avoiding them? Can you think of any physical improvements to the robot that will make it useful?

Racer Bot

There are several professionally designed robot kits for the micro:bit, and we will use one of them to give an example of how MicroPython can be used to command and control quite a sophisticated robot.

The Bit:Bot by 4tronix is another trundlebot but tricked out with a large number of features: NeoPixels, a single pitch buzzer, line following sensors, light sensors, a proper edge connector for the micro:bit, and a battery holder with a power switch (see Figure 12-5). Assembly takes minutes.

The Bit:Bot by 4tronix
Figure 12-5. The Bit:Bot by 4tronix

Control of the motors is more complicated than our homemade robot, since each motor has two connections: one for direction (forwards or backwards), the other for speed. The simplest way to make the motors move is to switch the pins to which they are connected to high:

pin8.write_digital(0)  # set direction to forwards
pin0.write_digital(1)  # set speed to full on

However, we probably want to change the speed of the motor, so instead we should replace the write_digital call on pin0 to its analog equivalent:

pin0.write_analog(511)

This is another appearance of PWM to drive something. Since the range for writing analog values is between 0 (off) to 1,023 (always on), then the preceding example sets the speed of the motor to half its potential full speed (since 511 is just under half of 1,023).

The 12 NeoPixels are connected to pin13 and use the standard neopixel module that comes with the micro:bit. The buzzer is monotonic (it only plays one note) and can be controlled with a digital signal via pin14. Reading digital values from pin 11 (left) and pin 5 (right) will indicate if a line is detected (allowing you to create a simple line following autopilot). The two light sensors are controlled by pin 16 (to select which one to use, 0 means left; 1 means right) and pin 2 (to read the analog value representing the amount of light detected).

It is possible to create a remote-controlled robot with two micro:bits connected using the radio module. This will require signalling between devices and a way to turn such interactions from the controlling micro:bit into outcomes on the remote controlled robot. In other words, there needs to be an intuitive user interface.

It should be possible to tilt the controlling micro:bit to indicate the direction of travel. For example, tilting the micro:bit forwards and left will cause the robot to move forwards and to the left. Furthermore, there should be some measurement of the degree of tilt so that the robot changes speed or angle of steering. One of the buttons should cause the buzzer to bleep like a sort of minimalist robot car horn, and the other could toggle the NeoPixels on and off. We’ll set the line following and light sensors aside for the moment.

Assuming the control outlined, a protocol for sending actionable information is still needed. We need some way to send signals for the speed, steering, the buzzer, and NeoPixels; in other words, four pieces of information. The simplest and easiest-to-understand solution would be to send a string with the four values delineated by a token such as “:”. If the place order of the values is pre-defined, the receiving robot can split the string at each instance of the “:” token and work with the resulting value. A message would look something like speed:steer:buzzer:neopixel, with each position being either an analog value (in the case of the speed and steer) or a digital value to indicate to sound the buzzer or toggle the NeoPixels. All that would remain is an agreement on the radio channel.

The following script for the controlling micro:bit implements all these features:

from microbit import *
import radio


radio.config(channel=44)
radio.on()


# Defines the range of valid tilt from accelerometer readings.
max_tilt = 1000
min_tilt = 199


while True:
    # Grab the inputs.
    y = accelerometer.get_y()  # Forwards / backwards.
    x = accelerometer.get_x() # Left / right.
    a = button_a.was_pressed()  # Horn.
    b = button_b.was_pressed()  # Toggle lights.

    # Data from the controller to be sent to the vehicle.
    # [speed, steer, buzzer, neopixel]
    control_data = [0, 0, 0, 0]
    if x < -min_tilt and y < -min_tilt:
        # forwards left
        display.show(Image.ARROW_NW)
        control_data[0] = max(y, -max_tilt)
        control_data[1] = max(x, -max_tilt)
    elif x < -min_tilt and y > min_tilt:
        # backwards left
        display.show(Image.ARROW_SW)
        control_data[0] = min(y, max_tilt)
        control_data[1] = max(x, -max_tilt)
    elif x > min_tilt and y < -min_tilt:
        # forwards right
        display.show(Image.ARROW_NE)
        control_data[0] = max(y, -max_tilt)
        control_data[1] = min(x, max_tilt)
    elif x > min_tilt and y > min_tilt:
        # backwards right
        display.show(Image.ARROW_SE)
        control_data[0] = min(y, max_tilt)
        control_data[1] = min(x, max_tilt)
    elif y > min_tilt:
        # backwards
        display.show(Image.ARROW_S)
        control_data[0] = min(y, max_tilt)
    elif y < -min_tilt:
        # forwards
        display.show(Image.ARROW_N)
        control_data[0] = max(y, -max_tilt)
    if a:
        # Sound the buzzer 
        control_data[2] = 1
    if b:
        # Toggle the NeoPixels 
        control_data[3] = 1
    if any(control_data):
        msg = '{}:{}:{}:{}'.format(*control_data)
        radio.send(msg)
    else:
        display.clear()
    sleep(20)

The bulk of the script is in an event loop in which any inputs from the accelerometer and buttons are read; and, depending on the readings from the accelerometer, the speed and steer values are set. If a button is pressed for the buzzer or NeoPixels, this is set as a flag. So the driver has feedback about what the micro:bit thinks it is doing and the display is updated with arrows to indicate the direction. Finally, if there’s any control data to send, it is transmitted via the radio.

Things are simpler for the micro:bit controlling the robot: it needs to consume the signal, decode it, turn the speed, and steer into signals to drive the motors and react to buzzer or NeoPixel signals:

from microbit import *
import radio
import neopixel


display.show(Image.SKULL)  # Logo :-)

colour = (244, 0, 244)  # NeoPixel colour to use for lights.
np = neopixel.NeoPixel(pin13, 12)
lights = False

radio.config(channel=44)
radio.on()


def move(speed, steer):
    # Sensible defaults that mean "stop".
    forward = 0
    left = 0
    right = 0
    if speed > 0:
        # Moving forward.
        forward = 1
        left = 1000 - speed
        right = 1000 - speed
    elif speed < 0:
        # In reverse.
        left = 1000 + (-1000 - speed)
        right = 1000 + (-1000 - speed)
    if steer < 0:
        # To the right.
        right = min(1000, right + abs(steer))
        left = max(0, left - abs(steer))
    elif steer > 0:
        # To the left.
        left = min(1000, left + steer)
        right = max(0, right - steer)
    # Write to the motors.
    pin8.write_digital(forward)
    pin12.write_digital(forward)
    pin0.write_analog(left)
    pin1.write_analog(right)


while True:
    pin14.write_digital(0)  # Switch off the horn
    try:
        msg = radio.receive()
    except:
        msg = None  # Networks are not safe!
    if msg is not None:
        # Get data from the incoming message.
        speed, steer, horn, light = [int(val) for val in msg.split(':')]
        move(speed, steer)  # Move the robot.
        if horn:
            # Sound the horn
            pin14.write_digital(1)
        if light:
            # Toggle lights
            if lights:
                np.clear()
                lights = False
            else:
                lights = True
                for i in range(12):
                    np[i] = colour
                    np.show()
    else:
        # No message? Do nothing!
        move(0, 0)
    sleep(20)

A move utility function is defined to hide away the common implementation details of setting the pins to drive the motors. It takes the transmitted speed and steer values and ensures that the correct direction of rotation and level of speed is sent to the motors so that the robot moves forward and backwards or steers left or right.

The event loop switches off the horn (so it will only make a short bleep sound), listens out for incoming radio messages, and, if a message is received, reacts to the content therein.

If you have more than one robot and enough micro:bits, you could organise a race. However, to stop interference from the wrong controller, you should ensure that the radio channel is different between each pair of devices. Furthermore, the robot displays a logo, so it’s probably a good idea to change that to a contestant number.

To end, I want to suggest a couple of enhancements to our robot to challenge your newfound robotics skills. If you press both buttons at the same time, it should be possible to toggle in and out of autopilot mode. The autopilot could work in two different ways: it could follow a line, or it could follow a light source. There are line- and light-detecting sensors on the chassis, so have a play and try to work out how to make it work.

1 This bot was designed by the exceptionally talented Radomir Dopieralski. Radomir was one of the many volunteers who helped bring MicroPython to the micro:bit. His passion is making robots for MicroPython boards, and you can find many examples of his work on hackaday.io.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.8.247