Robot Agents, Messages and The Society of Mind

One of the biggest challenges in both real and artificial minds is how to keep track of all the voices—both outside and in the head. The brain is continually bombarded by data from the outside world, inside the body (proprioception), and even from other parts of itself. How does such a system retain any kind of coherence without simply spinning off into madness?


In the early 1970s, Marvin Minsky and Seymour Papert, pioneers in the fields of cognitive psychology and artificial intelligence, reasoned that such a complex system could not be controlled by some single executive function. Of course, it sometimes seems like that's how it works; for example, you decide to go to the kitchen looking for a snack, you find your way to the kitchen without stepping on the sleeping cat, you open the refrigerator, etc. But of course, we are only aware of a tiny fraction of all the neural activity involved in making this happen. A path to the kitchen must be chosen from your current location, hundreds of muscles must be commanded to contract in a particular sequence, heart rate and breathing must be adjusted, balance must be maintained, obstacles must be avoided, the fridge handle must be grasped, foodstuffs must be identified, etc.


Minsky and Papert postulated that these various subtasks were taken care of by semi-autonomous agents which are themselves not particularly intelligent but are good at doing one particular task very well. Maintaining balance is a good example as is the task of reaching for an object. And even these tasks are rather complex and would likely be the result of even more specialized agents working together on even simpler tasks. Minsky referred to this interaction among agents as "a society of mind" and published a book by that title in 1986. In his view, mind is not any one process or quality of the brain, but precisely this complex interaction among mindless agents.


Minsky's and Papert's ideas significantly influenced the development of artificial neural networks, which as we have seen in earlier blog articles, are composed of many simple artificial neurons or nodes connected together into a larger network. Individually, a given neuron computes a relatively simple function on its inputs, but taken together, the entire network can solve more complex problems such as recognizing faces in a picture. Even so, artificial neural networks represent a fairly low-level form of agent processing since the agents themselves are computing such simple functions and the messages being passed are simple numbers. We can broaden the concept of an agent or node and the types of messages they can exchange to create even more intelligent systems.


The idea of a loosely interconnected collection of agents has become a popular programming model in robotics. This approach lends itself naturally to constructing a modular system of simpler components that can be combined and reused to produce more complex behaviors. In many of these systems, the agents are referred to simply as nodes, and nodes communicate with one another by passing messages. For example a sonar sensor mounted on the head of your robot could be considered a node while its message would consist of its current distance reading to the nearest object. A more complex node might consist of a head tracking routine (as we have seen in previous articles) that reads visual messages from a head-mounted camera and sends motor control messages to the head's pan and tilt servos. Nodes can also represent remote data sources, such as a web server where the messages are the contents of web pages, or a remote web camera so that our robot's sense of vision can be expanded to other locations.


Two of the more popular robot programming frameworks are built around nodes and messages. These are the Robot Operating System (ROS) from Willow Garage, and Microsoft's Robotics Developer Studio (MSRDS). While ROS runs primarily on Linux machines and is open source, MSRDS runs only on Windows computers. Both systems are very powerful but also require a significant investment in learning and practice. Fortunately, the key concepts of nodes and messages can be constructed very simply without having to adopt these entire frameworks. At the same time, our methods will not preclude us from using these frameworks at a later time if desired. What's more, the code developed below will run on all three platforms (Linux, Windows, and MacOS X) without modification.



Programming with Nodes and Messages

Our nodes will need a way to communicate with each other. For example, suppose one node monitors our robot's video camera and tracks an object of interest by recording its X-Y coordinates. A second node controls the pan and tilt servos attached to the camera and we want those servos to move in a way to keep the object centered in the field of view. When programming in a multi-threaded environment, it is important to prevent different nodes from overwriting a given variable at the same time. Otherwise, unpredictable values and behaviors can result; for example, a motor might suddenly start oscillating wildly or your entire program could lock up. Avoiding these problems is called making your code thread safe. Incidentally, even our own brains require this kind of separation of threads. For imagine trying to have a different conversation with two or more people at the same time. With two conversations, we might just be able to keep things straight as long as no one speaks too quickly. But throw in a third conversation and we will quickly become confused and frustrated.


There are many ways to achieve thread safety in your programming code. The one we will follow in this article requires that all nodes communicate with each other through a common "message board". The idea is that each node will either publish or read data in the form of messages that are posted in a single place, much like a message board mounted on a wall in school or the office. If one node needs data from another node, it does not contact the node directly, but instead looks for the appropriate message on the message board. By using a single location to store and retrieve messages between nodes, we can easily make our program thread safe.


A message has two simple parts: its topic and its value. For example, the message from a sonar sensor might look like this:


("head_sonar_reading", 33)


where the topic is "head_sonar_reading" and its value is 33 inches. Any node that wants to know the reading from the head sonar need only look up this message on the message board.


Messages can also represent a request. For example, to rotate the robot 60 degrees to the right, we don't talk to the drive motors directly, but post the request on the message board:


("rotate_base", 60)


If the robot's motor controller is set to monitor the message board, it will see such a message and act on it accordingly. Similarly, any other node that cares to know what the robot base is up to can read this message.


Message values do not have to be simple numbers. They can be text strings such as "Meet me at the coffee shop at 10", sets of instructions, lists of numbers, lists of other nodes, lists of lists, and so on. In our real-world message board analogy, you could post a written note or nail a lost shoe to the board. Either way, some one will get the point.


In the Python programming language, our message board is a simple dictionary where the keys are topic names and the values are the data or request values. The examples above would then become:


MessageBoard = dict({})

MessageBoard['head_sonar_reading'] = 33

MessageBoard['rotate_base'] = 60


For an example of a more complicated message, we might have a list of fruits such as:


fruits = ["banana", "orange", "peach", "blueberry", "apple", "raspberry"] 

which we could then post on the MessageBoard using the simple statement:


MessageBoard['fruits'] = fruits


The Message Board and Working Memory

The message board concept is loosely analogous to the concept of working memory in cognitive psychology. By laying out all the messages before us in one place, we can process them in a more orderly fashion, such as sorting them into categories, removing messages we don't care about, or re-ordering them into a sequence different from the order in which they arrived. If we include a time stamp with each message, we can even periodically clear out messages older than a certain date. We will have much more to say about this in later articles.



Publishing and Reading Messages

The main role of our most abstract node class is to publish and read messages to and from the message board in a thread-safe manner as shown in the Python code below:


(PLEASE NOTE: these examples require Python 2.6.5 or above to run.)

import threading

MessageBoard = dict({})

class Node(object):
    """ Top Level Node Class """
    
    def __init__(self, name="", uri=""):
        self.name = name
        self.uri = uri

    # Publish a topic-value pair on the Message Board
    def pub_message(self, topic, value):
        self.messageLock = threading.Lock()
        with self.messageLock:
            MessageBoard[topic] = value
    
    # Get the value of a topic from the Message Board.
    # Return None if the topic does not exist.        
    def get_message(self, topic):
        self.messageLock = threading.Lock()
        with self.messageLock:
            try:
                return MessageBoard[topic]
            except:
                return None



The function pub_message() takes two arguments, a topic and a value, sets a thread lock to keep the MessageBoard safe for the moment, then sets the topic to the given value. (The lock is released automatically as we fall through the end of the function.) Similarly, get_message() takes a topic, sets a lock, checks to see if that topic exists on the message board, and returns the appropriate value.


Here is an example using our head sonar.


HeadSonar = Node(name="Head Sonar")

HeadSonar.pub_message("head_sonar_reading", 33)

print HeadSonar.get_message("head_sonar_reading")


Any other node that would like to know the value coming from the head sonar can read up on the "head_sonar_reading" topic on the message board. For example, an obstacle avoidance routine might check this message several times a second. In the meantime, the obstacle avoidance routine does not need to know how to contact the sonar sensor directly; it simply reads the appropriate message(s) on the message board and assumes they are coming from a reputable source.


Programming Examples

The following code snippet illustrates two nodes running in separate threads and each publishing a message once a second while reading and printing the message of the other node:


from pi.nodes.node import Node
import threading, time

Node1 = Node(name="Node 1")
Node2 = Node(name="Node 2")

class Thread1(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.interval = 1
        self.count = 0

    def run(self):
        Node1.pub_message("node1_topic", "Hello from Node 1! Count: " + \
        str(self.count))
        print "Node 1 reads Node 2's topic: %s " % Node1.get_message("node2_topic")
        self.count += 1
        time.sleep(self.interval)
            
class Thread2(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.interval = 1
        self.count = 0

    def run(self):
        Node1.pub_message("node2_topic", "Hello from Node 2! Count: " + \
        str(self.count))
        print "Node 2 reads Node 1's topic: %s " % Node2.get_message("node1_topic")
        self.count += 1
        time.sleep(self.interval)

thread1 = Thread1()
thread2 = Thread2()

thread1.start()
time.sleep(2)
thread2.start()


The output should look like this:

Node 1 reads Node 2's topic: None
Node 1 reads Node 2's topic: None 
Node 1 reads Node 2's topic: None 
Node 2 reads Node 1's topic: Hello from Node 1! Count: 2 
Node 1 reads Node 2's topic: Hello from Node 2! Count: 1 
Node 2 reads Node 1's topic: Hello from Node 1! Count: 3 
Node 2 reads Node 1's topic: Hello from Node 1! Count: 4 
Node 1 reads Node 2's topic: Hello from Node 2! Count: 2 
Node 1 reads Node 2's topic: Hello from Node 2! Count: 3 
Node 2 reads Node 1's topic: Hello from Node 1! Count: 5 

etc.


Note that because we started Node 2's thread 2 seconds after Node 1, the value of its topic is "None" until the thread is started. Note also that the messages do not exactly alternate between Node 1 and Node 2 even though they are both running on a 1 second interval. This is because the exact instant at which the print command is executed in each thread is not under our control. However, even so, note that the count values increment correctly for each of the two topics so we are not actually missing any data.


Here is a another example that mimics more closely what we might do with our robot. The first node publishes a random number between 1 and 10 simulating a sensor reading. The second node reads this value on the MessageBoard and commands the robot to turn left if the number is odd or right if it is even. Furthermore, we set the publishing rate for Node 1 to 100 times per second (interval = 0.01 seconds) to illustrate that even very fast message updates are no problem for our thread locking mechanism.


from pi.nodes.node import Node
import threading, time, random

Node1 = Node(name="Node 1")
Node2 = Node(name="Node 2")

class Thread1(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.finished = threading.Event()
        self.interval = 0.01
        self.daemon = False

    def run(self):
        while not self.finished.isSet():
            Node1.pub_message("node1_topic", + random.randrange(11))
            time.sleep(self.interval)
            
    def stop(self):
        print "Stopping Node 1 Thread ...",
        self.finished.set()
        self.join()
        print "Done."
            
class Thread2(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.finished = threading.Event()
        self.interval = 0.5
        self.daemon = False

    def run(self):
        while not self.finished.isSet():
            node1_message = Node2.get_message("node1_topic")
            if node1_message % 2 == 0:
                Node2.pub_message("node2_topic", "Turn Right")
            else:
                Node2.pub_message("node2_topic", "Turn Left")
            print "Node 1 Message:", node1_message, "Node 2 Message:", \
            Node2.get_message("node2_topic") 
            time.sleep(self.interval)
            
    def stop(self):
        print "Stopping Node 2 Thread ...",
        self.finished.set()
        self.join()
        print "Done."

thread1 = Thread1()
thread2 = Thread2()

thread1.start()
thread2.start()

time.sleep(10)

thread1.stop()
thread2.stop()


And the output will look something like this:

Node 1 Message: 10 Node 2 Message: Turn Right
Node 1 Message: 8 Node 2 Message: Turn Right
Node 1 Message: 0 Node 2 Message: Turn Right
Node 1 Message: 3 Node 2 Message: Turn Left
Node 1 Message: 9 Node 2 Message: Turn Left
Node 1 Message: 1 Node 2 Message: Turn Left
Node 1 Message: 0 Node 2 Message: Turn Right
Node 1 Message: 3 Node 2 Message: Turn Left
Node 1 Message: 9 Node 2 Message: Turn Left
Node 1 Message: 10 Node 2 Message: Turn Right
Node 1 Message: 4 Node 2 Message: Turn Right
Node 1 Message: 10 Node 2 Message: Turn Right
Node 1 Message: 4 Node 2 Message: Turn Right
Node 1 Message: 10 Node 2 Message: Turn Right
Node 1 Message: 3 Node 2 Message: Turn Left
Node 1 Message: 6 Node 2 Message: Turn Right


Remote Messaging over a Network

So far we have designed our nodes and message board to operate on a single computer. Some roboticists prefer to work with a distributed computing architecture so that nodes can run on multiple computers yet still exchange messages. Fortunately, it is easy to plug in almost any function you like for pub_message() and get_message() in our node definition, including functions that pass the messages over a network connection. Furthermore, Python makes working with network communication fairly straightforward. Even so, this is a topic outside the scope of the current article so we will have to come back to it at a later time.



What Does Pi Robot Think of all This?

You'll be happy to hear that Pi Robot really likes the idea of using nodes and messages to control his behavior. For one thing, it allows him to take his mind off every little detail and spend more time surfing the web. As you know, I have recently converted most of Pi's programming code from C# to Python and I have also restructured his control architecture to take advantage of the message board concept described above. So how well does it work in real life? In short, it is a smashing success. In fact, the combination of Python and message-passing works much more smoothly than my older procedural methods using C#. And most importantly of all, the voices in his head are now making sense.