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.

“Lead and Follow”, Wikipedia

(part 2 can be found here)

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.