Lead and Follow (or, making low-cost robots dance), Part 1
In some types of partner dance, lead and follow are designations for the two dancers comprising a dance couple. […] The Lead is responsible for guiding the couple and initiating transitions to different dance steps and, in improvised dances, for choosing the dance steps to perform. The Lead communicates choices to the Follow and directs the Follow by means of subtle physical and visual signals, thereby allowing the couple to be smoothly coordinated.
This is an Edison. Edison is a cheap, rugged, programmable and LEGO-compatible robot.
Edisons have two motors, which can drive the wheels independently, an optical sensor on the base, and an infrared beam and sensor which is used for both obstacle detection and messaging. For output, they have a piezo transducer and two individually controllable LED lights at the front. There’s lots of smart thinking gone into the design of these robots. For example, they come with an in-built program that let them learn the signals from standard remote controls, so use your TV remote to drive the robot around.
There are three ways to write your own programs for an Edison. EdBlocks is a simple drag-and-drop system inspired by Scratch, EdWare is a bit more similar to the LEGO Mindstorms programming tools, and EdPy is a simplified version of Python. These are all available as web-browser-based interfaces.
(UPDATE: Lego’s Robot Inventor now supports Python too!)
Right from the beginning, I’d planned to get two of them, because I figured it would be fun to get them interacting. My son asked if it would be possible to get the robots performing synchronised dance moves. Now, obviously, we could load the same program into both, but I’ve always been intrigued by swarm robotics, so wouldn’t it be more fun if I could get them to self-organise? What if I could get one of them to teach the moves to the other?
This did not go 100% smoothly.
The actual code is small and simple (which is good, because I want to use it for teaching). The tricky bit was understanding what was physically happening. I haven’t played with robots much before. My professional programming is mostly done in a very abstract environment, where the details of the machine are handled for me by layers of operating system, frameworks and libraries. If I want to send information from one place to another, I can use a library to do it, and not worry about how the electronics will make that happen. But when I’m sending a beam of infrared light from one robot to another, I have to worry about it missing, or being reflected back to the robot it came from!
Edison already has support for reliable single-byte messaging over IR (using the Sony IR codes), but I’d need to devise a way of sending multi-byte strings of commands, including detecting and dealing with accidentally dropped bytes. Neither EdWare nor EdBlocks really seemed flexible enough for this, so it’d have to be EdPy.
EdPy’s Edison API
is accessed using the Ed
class, and it comes with a handy, general-purpose method for controlling
the motors:
Ed.Drive(direction, speed, distance)
Each parameter is a single byte. The direction
parameter can control
one or both motors: it has values like Ed.FORWARD
(both motors),
Ed.BACKWARD_RIGHT
(one motor) and Ed.SPIN_RIGHT
(both motors working
in opposite directions). The speed
ranges from 1-10, and the distance
is measured in either centimetres, inches or milliseconds, depending on what you set
Ed.DistanceUnits
to. If you set Ed.DistanceUnits
to centimetres or
inches, when turning the distance
is measured in degrees.
So, in theory, all I had to do was send a series
of three-byte sequences between the Edisons. However, I started off by assuming
that all moves took place at the same speed, simplifying that to just two
bytes for direction and distance.
Throughout this series of posts, I’m going to refer to the two Edisons (and the programs they’re running) as the Leader and Follower. Let’s start with the basic boilerplate that gets the robot into the right state. This is the same for both of them:
#-------------Setup----------------
import Ed # Import the Edison library
Ed.EdisonVersion = Ed.V2 # Which version of the Edison robot are we using?
Ed.DistanceUnits = Ed.CM # When driving the motors, measure distances in cm
Ed.ReadIRData() # Clear any existing data out of the IR register
The last line is worth mentioning. The Edison hardware is continually
checking for new bytes being transmitted over IR, and storing them in a hardware register
that can be read by calling Ed.ReadIRData()
.
Calling this method consumes that value, resetting the value in the register to 0
.
The next problem: based on the assumption that it wasn’t going going to work first time (and it didn’t), how do I debug it? Edisons don’t have a screen, or a serial or USB interface. You program them using a 3.5mm audio jack. That means you can’t get them to print out useful information when things go wrong. I eventually settled on getting them to flash their LEDs when receiving or sending a byte of data. Again, this is the same for both Leader and Follower:
def showSent():
Ed.RightLed(Ed.ON) # The LED will stay on until turned off
Ed.LeftLed(Ed.OFF)
def showReceived():
Ed.LeftLed(Ed.ON)
Ed.RightLed(Ed.OFF)
The brute force approach would be to have the Leader just send bytes as fast as it can and hope the Follower can keep up. This obviously won’t work! Bytes get lost, and the Leader has no way of telling this has happened, so can’t resend. We need a protocol for sending and acknowledging the receipt of data, so both Leader and Follower will need to be able to send and receive data:
def send(number):
Ed.SendIRData(number) # Send a single byte of data over infrared
showSent()
def recv():
r = Ed.ReadIRData() # Read a single byte of data from the infrared receiver
if r != 0: # If no byte has been received, this will be 0
showReceived()
return r
Ed.ReadIRData()
is
non-blocking:
if no IR data has been received since the
last call, it’s going to return 0
immediately, instead of waiting for some data to arrive. EdPy does have a way
to wait for IR data, but I might talk about that in another post.
What about actually sending and receiving the data? Here’s the Leader:
#-------------Leader----------------
dataFinished = 254 # End-of-stream terminator
moves = Ed.List(6); # The only way to create lists in EdPy
#------The moves we want to transmit
moves[0] = Ed.FORWARD
moves[1] = 2
moves[2] = Ed.SPIN_RIGHT
moves[3] = 32
moves[4] = Ed.SPIN_LEFT
moves[5] = 13
commands = 6
index = 0
while index < commands:
sendReliably(moves[index])
index = index + 1
sendReliably(dataFinished) # Let the other robot know we're finished
Ed.LeftLed(Ed.OFF)
Ed.RightLed(Ed.OFF)
This pretty simple, but there’s a couple of points worth discussing. To begin with, I define a byte value that signals the end of the command stream.
EdPy doesn’t support
all of the Python list features. Most things work, but you’re dealing with
a memory-constrained environment. You can’t create lists larger than 250
items, and you can’t change the size of a list once it’s been created. All
lists have to be allocated using the Ed.List()
method.
There’s a little wrinkle here, which I’ve not managed to find in the documentation, but I worked it out by trial and error. Edison seems to have a global working memory that’s 250 bytes in size, and all global variables (including the lists) are stored in that. So every variable you create decreases the maximum list size by one. And because you need a variable to hold the reference to the list, then your actual maximum list size is 249. This means that this works:
myVariable = Ed.List(249)
But this:
myVariable = Ed.List(250)
Generates this (fairly cryptic) error message from the EdPy compiler:
0, 0: Overflowed W memory
And so does this:
myVariable1 = 0
myVariable2 = Ed.List(249)
Let’s wrap up this part by looking at the equivalent code for the Follower:
#-------------Follower----------------
dataFinished = 254 # End-of-stream terminator
moves = Ed.List(242) # The follower can receive 121 commands
index = -1
received = 0
while received != dataFinished:
received = readReliably()
if received != dataFinished:
index = index + 1
moves[index] = received
Ed.LeftLed(Ed.OFF)
Ed.RightLed(Ed.OFF)
replay = 0
while replay < index: # Now replay the received commands
moveCommand = moves[replay]
moveAmount = moves[replay+1]
replay = replay + 2
Ed.Drive(moveCommand, Ed.SPEED_5, moveAmount)
I haven’t even bothered at this stage to check that the Leader doesn’t send too many bytes and overflow the receiving list on the Follower. Because the maximum size (with the variables I’ve declared) of a list is 243, the robot can receive 121 two-byte commands.
You might have noticed that I haven’t defined sendReliably()
or readReliably()
yet. In between my first attempt and the working version of the program, none of the code
I’ve shown so far changed at all. sendReliably()
and readReliably()
were the only two functions that I had to modify. The next post in the series
is going to talk about those, and also why the terminal byte is defined as 254
and not, for instance, 255
.