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.
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:
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).
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.
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?
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.
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.
3.141.8.247