Robot Architecture and Control

Kevin J. Walchko

created 20 Oct 2017


Now we are going to start getting the Roomba to move. In order to do that, we need to program the robot correctly. Students usually are lazy and write a lazy software flow for their robot and then don't understand why things crash or don't work like they want. Once we understand the Create 2 API and program a modular/logical software flow, we will develop a simple algorithm to get our robot to do obstacle avoidance.

Objectives

  • understand how to get the Roomba to move
  • understand simple obstacle avoidance
  • understand a simple mobile robot software flow
  • simple introduction to ROS

References

Setup


In [1]:
%matplotlib inline
from __future__ import print_function
from __future__ import division
import numpy as np

Robot Software Architecture

The Robot Operating System (ROS) is a flexible framework for writing robot software. It is a collection of tools, libraries, and conventions that aim to simplify the task of creating complex and robust robot behavior across a wide variety of robotic platforms.

Why? Because creating truly robust, general-purpose robot software is hard. From the robot's perspective, problems that seem trivial to humans often vary wildly between instances of tasks and environments. Dealing with these variations is so hard that no single individual, laboratory, or institution can hope to do it on their own.

As a result, ROS was built from the ground up to encourage collaborative robotics software development. For example, one laboratory might have experts in mapping indoor environments, and could contribute a world-class system for producing maps. Another group might have experts at using maps to navigate, and yet another group might have discovered a computer vision approach that works well for recognizing small objects in clutter. ROS was designed specifically for groups like these to collaborate and build upon each other's work.

  • ROS pros
    • Free (BSD licensed) robotic architecture
    • Distributed architecture, supports both multi-process/core and multi-machine
      • Publish/subscribe architecture allows this flexibility
    • C++ and python interfaces
    • Broad academic adoption
  • ROS cons
    • Rapid development cycle, leads to code working one year and broken the next
      • Or you can just stay on an older version of ROS, but then you loose new advancements
    • Steep learning curve ... nothing is simple
    • Only can develop/run on Ubuntu Linux
      • I spent years trying to keep the macOS version running and it was painful
    • Many packages written by Universities and are not always maintained to the current version or support is often lacking support/documentation
    • Updated annually, but core packages do not always use the most current version for libraries
    • Complex build system with Catkin
    • Designed for relatively powerful computers and not optimized for small embedded devices like Raspberry Pi
    • No security, robots easily hacked
    • Reliant on roscore as the central pub/sub broker, moving large amounts of data around in messages (i.e., 3D lidar point clouds, large images, etc) can introduce unnecessary delays and CPU overhead
      • ROS takes great pains to optimize the software and use smart data structures, algorithms, and techniques to reduce this overhead
      • Note: this can also be overcome by development of few nodes and smart partitioning of algorithms. However, this is not the default ROS mentality

This class will not throw you into the deep end with ROS. Instead we are just using Python, but you can take these ideas and use them when you build a complex robot.

  • Write simple programs that do one thing (python: multiprocessing)
    • easy to test and debug
  • Connect up your processes up with a messaging architecture
    • ZMQ pub/sub
    • Google protobufs
    • Events, Queues, Namespaces, etc
  • Design tests that can use your messaging architecture
    • Help with integration and all of the things you missed
  • Don't go too crazy and make a million processes, but make a few
    • increases robutness, one thing fails, the rest of the software keeps going
    • in testing, you can shutdown parts of your system, but keep others going or develop them on the fly

Threads vs Processes

Python, like C/C++ and most other languages support both threads and processes

  • Threads
    • Allows you to write simple functions that are easy to debug and can be dedicated to a CPU core
    • Each thread shares the same memory space, so passing variable between them is easy
    • However, Python has implemented threads such that they only run in one process and really don't do multiprocessing. This is called the GIL, global interrupt lock. It is either hated or tolerated by python programmers
    • When the main program exits, all of its threads exits
  • Processes
    • These are separate programs, each with their own memory space
      • If you want to share data between processes, take a look at events, queues, and namespaces ... you cannot share global variables
    • They can all be setup to run and stop individually. You can have a python program spin off processes that act like unix daemons (long running background processes) and they will stay alive even after the main python program exits
      • Danger: you can create zombie process that hang around until you specifically kill them or the system reboots
    • Because of the GIL, generally this is what people use if they want to do C/C++ type multiprocessing

Multiprocessing in Python

If you are interested in doing something like this, google: python multiprocessing and a bunch of examples will come up.

The example below, creates a new process that uses a shared namespace with other processes. This bridges the gap between threads (which share global variables) and processes that typically don't.

Queues

#!/usr/bin/evn python

import multiprocessing

class MyFancyClass(object):

    def __init__(self, name):
        self.name = name

    def do_something(self):
        proc_name = multiprocessing.current_process().name
        print 'Doing something fancy in %s for %s!' % (proc_name, self.name)


def worker(q):
    obj = q.get()
    obj.do_something()


# you have to have this if statement here 
if __name__ == '__main__':
    queue = multiprocessing.Queue()

    p = multiprocessing.Process(target=worker, args=(queue,))
    p.start()

    queue.put(MyFancyClass('Fancy Dan'))

    # Wait for the worker to finish
    queue.close()
    queue.join_thread()
    p.join()

Namespaces

#!/usr/bin/evn python

import multiprocessing

def producer(ns, event):
    ns.value = 'This is the value'
    ns.array = [1,2,3]  # for dynamic objects, you need to assign them to update the namespace
    ns.fail.append(1)   # does not update the global namespace! Do the above method

    event.set()  # let the other process know it is ok

def consumer(ns, event):
    try:
        value = ns.value
    except Exception, err:
        print 'Before event, consumer got:', str(err)

    event.wait()
    print 'After event, consumer got:', ns.value

if __name__ == '__main__':
    mgr = multiprocessing.Manager()
    namespace = mgr.Namespace()
    namespace.fail = []  # this is setup to show you a failure

    event = multiprocessing.Event()  # use for communications between processes

    p = multiprocessing.Process(target=producer, args=(namespace, event))
    c = multiprocessing.Process(target=consumer, args=(namespace, event))

    c.start()
    p.start()

    c.join()
    p.join()

The output is:

$ python multiprocessing_namespaces.py

Before event, consumer got: 'Namespace' object has no attribute 'value'
After event, consumer got: This is the value

Making the Create Move

So we have already talked about mobile robots, coordinate systems, body frames, etc. Now we are going to talk about how do we command the robot and get it to go where we want.

from  pycreate2 import Create2

# Create a Create2.
port = "path-to-serial-port"
bot = Create2(port)

# Start the Create 2
bot.start()

# Put the Create2 into 'safe' mode so we can drive it
# This will still provide some protection, when it senses danger it will stop
bot.safe()

# directly set the motor speeds ... go forward
bot.drive_direct(100, 100)  # inputs for motors are +/- 500 max
time.sleep(2)

# turn in place, CW
bot.drive_direct(200,-200)
time.sleep(2)

Simple Autonomous Behavior

Let's start off simple. Let's define a couple of states and transitions between them. Understand there is more than one way to do this, but we will give you an example.

  • Start: The initial state. On start-up, the robot will initialize and setup some things, then immediately tranision the Wander state.

  • Wander: When the robot enters into this state it starts to wander around.

    • This basically goes on forever, or until the batteries run out or someone turns the robot off. Although this might sounds useless, this is actually useful. If a robot has the ability to map its world, but doesn't start off with a map (maybe it just landed on Mars), wandering around and "seeing the sights" is a great way to map the world.
    • Wander could always just head in one direction, or maybe it randomly chooses another direction after a random about of time ... who knows! What it does is up to the programmer and how complex it is.
    • Wander might also decided where it wants to go based on "holes" in its current map
    • Interesting fact, iRobot just announced that Roomba will begin mapping the strength of your home WiFi as they move around and vacuum your house.
  • Avoid Obstacle: Basically what it does is checks to see if there are any obstacles or dangers around and then avoids by some means.
    • Turning: you can always turn in one direction (i.e., right turns) or alternate turns.
    • Other: if your robot isn't able to detect obstacles very far in-front of it, you could back up a little bit first, to give the robot some room, before the robot turns. The last thing you want is to turn and have one of your wheels drive off a cliff.
    • As your robot and its task becomes more complex, you will probably do more than just a simple turn.

Let's maybe try to make it a little more robust. Now we create a couple more transitions.

  • Stop: There is some simple measureable goal we want our robot to reach. When it meets it (i.e., completely explore its environment, vacuum a room, battery level reaches a certain level, etc), it transitions from Wander to Stop. Stop obviously does the opposite of start and cleanly turns things off.
  • Stuck: basically this is saying, after so many continous turns, the robot is stuck and needs human intervention. Maybe the robot has somehow become trapped in a box and can't get out.
    • Below was a test conducted in the UK, where an autonomous car was designed to understand common road markers and the dashed/solid line was a typical commuter lane marker. The car knew it could cross the dotted line (ignoring the solid line) to get into the circle. However, once it was inside, the only way to get out was to cross a solid white line. This was aginst the car's programming, because it meant the car was not allowed to exit the commuter lane at this location. It has to wait until it sees dashed white lines.

Suggested Simple Robot Architecture for Your Roomba

Actually you can write a rather professional, modular, and clean architecture with Python. Remember to always set up your system properly and tear it down when you shutdown. Killing your software with Ctrl-C (essentially causing it to crash) is sloppy and can leave you in a bad state.

Think about this. Your program just commanded the robot to go forward at full speed. Then you quit your program. There was never a command sent to the robot to stop ... oops. So try to think about this when you program your robot. There are some safety things I have put in the code so you don't do anything too stupid, but I have not robustly tested the Roomba software either.

The following software suggestions are primarily functional programming. However, I have also mixed in some class programming too. Feel free to program however you want.

File: states.py

This basically holds the functions (or classes if you prefer) that tell your robot what to do. There is a function for each state.

#!/usr/bin/env python

from __future__ import print_function, division

def Start(robot):
    # setup things for our robot
    robot.bot.camera = Camera('pi')
    robot.bot.camera.init(window=(640,480))
    # stuff happens here
    return 'STATE_WANDER'

def ObstacleAvoid(robot):
    # for our simple 2D robot, this function isn't too complex
    # read sensors
    sensors = robot.bot.get_sensors()
    # stuff happens here  
    next_state = 'STATE_WANDER'
    return next_state

def Wander(robot):
    # remember, this could be really complex code
    # stuff happens here
    return 'STATE_OBSTACLE_AVOID'

def Stop(robot):
    # shutdown things for our robot
    return 'STATE_OBSTACLE_AVOID'

# this simple hash is just a simple way to keep track of everything
# it is not necessary to do this, but can help to keep things organized.
StateArray = {
    STATE_START: Start,
    STATE_OBSTACLE_AVOID: ObstacleAvoid,
    STATE_WANDER: Wander,
    STATE_STOP: Stop
}

# Now you can access these using:
#
# StateArray[STATE_START](robot)
#
# Or, if you hate hashes, use an array like this:
#
# StateArray = [Start, ObstacleAvoid, Wander, Stop]
#
# Thn you could access your functions like this:
#
# StateArray[0](robot)
#
# Note, in this method, you have to keep track of what index 0, 1, 2, ... is. 
# Either way will give you the same thing, but the first is more readable

Notice the interface for each of these is the same robot argument. This is nice, because it leads to modular code! I can easily add new states without changing any other function or code because all of the states take the same inputs. This is good design, but it takes planning to do this.

File: robot.py

Here is your robot file that acts like the actual state machine and determines how you transition to the next state based off of outputs from the previous state.

#!/usr/bin/env python

from __future__ import print_function, division
from states import StateArray
from pycreate2 import Create2    # Roomba driver
from nxp_imu import IMU          # imu driver
from time import sleep

class MyRobot(object):
    """
    This is a super simple class to hold things together.

    Unfortunately you are not trained like the rest of the world to
    program with classes, so I want to keep this simple. However, if
    you have any talent, then please try to do this properly. In 1993
    at Univ of Florida we spent half of the semester in our C++ class
    learning Object Oriented Programming (OOP). If you took the data
    structures class, then you learned Java ... all class based.
    """
    bot = None
    camera = None
    imu = None

    def __init__(self, port):
        # this function is automatically run when I call MyRobot()
        # It sets up everything I need
        self.bot = Create2(port)

    def __del__(self):
        # this function is automatically called when MyRobot() goes
        # out of scope or the program is ended
        self.bot.safe()
        self.bot.close()
        self.camera.close()
        self.imu.close()

if __name__ == "__main__":
    port = "/dev/ttyUSB0"  # serial port path, change as appropriate for your robot
    robot = MyRobot(port)
    current_state = 'STATE_START'

    try:
        while True:
            current_state = StateArray[current_state](robot)
            sleep(0.1)  # run this loop at 10 Hz forever or until Ctrl-C is pressed
    except KeyboardInterrupt:
        print('User hit Ctrl-C, robot shutting down\n\nBye ...')
        # depending on how you program things, you might have to tell the robot
        # to stop

Now, for whatever reason, I broke up my state function into one python file (like a library) and my actual run-time loop into another. You don't have to do that. I try to keep things organized and as stuff gets more complex, I break them out into different files so I don't have just one python file with 1000 lines of code in it.

Obstacle Avoidance

Ok, so you did some obstacle avoidance in ECE 382. Depending on how well you did (or how much help you got from your friends) you maybe made it through the maze. Basically, we will start there.

The block Turn seems simple ... yes it is. We are starting simple. Later when we do path planning, this could be much more complex (you will see). Think how complex it would be if we were using quadcopters and we detected an obstacle in 3D space as opposed to our Roomba who lives on a 2D floor. Remember, we are starting simple like ECE382 robot maze.

  • How could you improve this?
  • How long should we sleep for?
    • What effects the number we pick?
  • Is sleep the best way?
    • Does our robot do anything while we sleep? hint: this is a trick question

Exercise

  1. Turn on and login to your roomba and see if you can drive it around. The pycreate library has some simple examples for you to follow.
    • If you do play with the Roomba, please:
      • put it back on the charger properly
      • shutdown the RPi properly
      • turn off the RPi's battery pack

Questions

  1. What is ROS?
    1. What are some of the pros/cons associated with it?
  2. What is the difference between processes and threads
  3. What is different between C/C++ threads and Python threads?
  4. If you had to write a simple robot architecture (maybe on a GR) based off of what we talked about in this class. What would you write? Make sure it has the following states:
    1. Start
    2. Wander
    3. Avoid
    4. Stop
  5. Looking at the pycreate reference link above, what type of sensors are available for you to access?