Tuesday, February 18, 2014

User interaction with chat interface

Previously I showed how to communicate between Python and the Arduino. Next we need a way for the user to interact with the Arduino, via the Python interface. The user might be a person, typing commands into a terminal, or it might be another script. Ultimately we will want to control the entire behavioral shaping process from another script, which for example will observe the subject's performance and adjust future trials accordingly.

For reading: Let's write out everything the arduino says to a text file. This way we'll have an exact record of what happened. Other programs can parse this file for the results so far.

For writing: This was a little more difficult. I wanted a general interface that could be used by a human or another script to communicate with the Python chatting script. I decided to use a named pipe, which is similar to a file. Anything that is written to this file-like object will be picked up by the chatting script and communicated to the Arduino, line by line.

Here's the old code as well as the new code:

## From device to user
def read_from_device(device):
"""Receives information from device and appends"""
new_lines = device.readlines()
return new_lines
def write_to_user(buffer, data):
"""Write `data` to the user via `buffer`
`buffer` : a file
"""
# No matter what, pipe new_lines to savefile here
for line in data:
buffer.write(line)
buffer.flush()
## From user to device
def read_from_user(buffer, buffer_size=1024):
"""Read what the user wrote and returns it
`buffer`: a named pipe
"""
try:
data = os.read(buffer, buffer_size)
except OSError as err:
# Test whether this is the expected error
if (err.errno == errno.EAGAIN or
err.errno == errno.EWOULDBLOCK):
# Expected error when nothing arrives
data = None
else:
# Unexpected error
raise
return data
def write_to_device(device, data):
if data is not None:
device.write(data)
view raw chat.py hosted with ❤ by GitHub
The syntax for reading from the pipe is a little clunky. I wanted it to be non-blocking, because otherwise the program halts if there is nothing to read. But the non-blocking read will raise an error whenever no data is found. So we catch that exception and ignore it.

To create the pipe named TO_DEV, we just need to run:
    os.mkfifo('TO_DEV')

Now you can write to the pipe, either from another terminal like so:
    echo "MESSAGE" > TO_DEV
This automatically appends a newline.

Or from another Python script:
    pipeout = os.open('TO_DEV', os.O_WRONLY)
    os.write(pipeout, 'MESSAGE\n')
Here you have to add your own newline.


Next time I'll package this up into a complete script and demonstrate it.

Monday, February 17, 2014

First step -- a chat interface

The first thing we need is a way to communicate between python and the arduino over the serial port.

Python side

The `pyserial` library makes things easy here.
## Set up device
# 0 means return whatever is available immediately
# otherwise, wait for specified time
# 0.01 takes a noticeable but small amount of CPU time
self.ser = serial.Serial(serial_port, 9600, timeout=serial_timeout)
# Wait for it to initialize the arduino
time.sleep(0.1)
self.ser.flushInput()
## From device to user
def read_from_device(device):
"""Receives information from device and appends"""
new_lines = device.readlines()
return new_lines
## From user to device
def write_to_device(device, data):
if data is not None:
device.write(data)
view raw chat.py hosted with ❤ by GitHub

The `timeout` keyword is a key element here. That defines how long `readlines` will wait for input before returning. If you set it to 0, it will return whatever it finds immediately. The problem with that approach is that we're going to be calling this function over and over again in an infinite loop. I suspect a smaller number of calls with a small delay will end up being more efficient than a much larger number of calls with a near-zero delay. That's something to test layer.

The `write` function actually writes immediately and returns ("non-blocking").

The Arduino side

The arduino needs something to say. Let's have it report the time every second. Also, let's have it acknowledge any chat it receives, simply by echoing back whatever it hears.
#include "chat.h"
void setup()
{
Serial.begin(9600);
Serial.println("Running setup.");
}
unsigned long speak_at = 1000;
unsigned long interval = 1000;
void loop()
{
unsigned long time = millis();
// Announce the time, occasionally ending with a newline
if (time > speak_at)
{
/*
if (speak_at % 3 == 0)
Serial.println(time);
else
Serial.print(time);
*/
Serial.println((String) "The time is " + time);
speak_at += interval;
}
// Receive any chat
String received_chat;
received_chat = receive_chat();
if (received_chat.length() > 0)
{
// Do something with it
Serial.println((String) "You sent a string of " +
received_chat.length() + " characters");
}
}
view raw Chat.ino hosted with ❤ by GitHub

The actual `receive_chat` function resides in a library.
#include "Arduino.h"
#include "chat.h"
String receive_buffer = "";
String receive_line = "";
String receive_chat()
{
// If characters available, add to buffer
int n_chars = Serial.available();
char got = 'X';
String return_value = "";
for(int ichar = 0; ichar < n_chars; ichar++)
{
got = Serial.read();
if (got == '\n')
{
// Add the newline
receive_buffer += got;
// Put buffer in line
// Start buffer over
receive_line = receive_buffer;
receive_buffer = "";
break;
}
else
{
receive_buffer += got;
}
}
// If a line available, echo it
if (receive_line.length() > 0)
{
Serial.print("ACK ");
// Strip the newline
// This needs to take Windows newlines into account too
Serial.print(receive_line.substring(0, receive_line.length()-1));
Serial.println("");
return_value = receive_line;
receive_line = "";
}
return return_value;
}
view raw chat.cpp hosted with ❤ by GitHub

Things are more complicated here than in Python because we have only low-level functions available. Whenever there are new characters available, we add them to a global variable `receive_buffer`. It has to be global because it needs to persist over many calls to this function.

Once the buffer gets to a newline, it stops taking characters and returns the line it received. It also echoes it on the serial port so that Python knows its message was received.

What if multiple lines are waiting to be received? This will only return the first one. This is easier than coding up a linked list of multiple commands. It also means that other things have a chance to run on the rest of the update cycle.



Now the arduino can communicate with Python. But how does Python interact with the user? The user might be a real person, or it might be another Python script, for instance one controlling the state machine. The next post will cover the methods by which the user can read what the arduino is saying, and type in responses.