Embedded Interface Testing with Python: PyUnit and PySerial

2005 O'Reilly OSCON, Portland, Oregon

Kees Cook

OSDL

or..

How to make sure you didn't break anything

Creative Commons Attribution-NonCommercial-ShareAlike License

Who am I?

Kees Cook (that's pronounced “case”, BTW)

@ work:

kees@osdl.org

http://developer.osdl.org/kees/

@ home:

kees@outflux.net

http://outflux.net/

http://outflux.net/blog/

on IRC

“keescook” on FreeNode

Past Experience

Firmware Development

SnapOn “MODIS” vehicle analyzer

http://www.snapon.com/Modis/

CMUcam2 servo-controlled vision system

http://www.active-robots.com/products/accessories/cmucam.shtml

Reach Technology “SLCD” touch-screen interface

http://reachtech.com/display/slcd-kits.html

Sendpage: modems, paging terminals

http://sendpage.org/

Summary

Background

Device

Requirements

Plan

Driver

Communication

Operation

Testing

Unit Tests

Background: The Device

Hardware: “SLCD”

Serial interface (surprise!)

LCD

Touchscreen

Software: Custom Firmware

Drawing primitives

Touch reporting

Background: The Requirements

Goals

Regression Testing

Portability

May have multiple platforms, multiple developers

Flexibility

Need to add new tests quickly

Solution: Python

Portable (Linux, Windows, OSX, *nix)

Flexible (Scripted language)

New to me!

Background: The Plan
(How to Build an Embedded Tester)

Communication (hard)

Need to reliably talk to the hardware

Operation (tricky)

Need to develop a library to operate the device

Testing (slow, but easy)

Write tests for the library (not the hardware)

Serial communication

Continues to be the standard

Lightweight, “universal”

Car diagnostic scanners, robotics, GPS, consoles

I am easily frustrated by serial communication

But I maintain Device::SerialPort Perl module!

I love PySerial

http://pyserial.sourceforge.net/

Currently ported at least to Linux, Windows, BSD

Works like a charm

Not responsible for it

PySerial Methods

Using only a small subset of available methods:

Serial(port=None, baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=None, xonxoff=0, rtscts=0, writeTimeout=None)

flushInput()

write(data)

readline(self, size=None, eol='\n')

PySerial Initialization

import serial, sys, slcd

try:

port = serial.Serial(

port=3, # 0-based port numbering

baudrate=115200,

rtscts=0,

xonxoff=0,

timeout=1, # seconds

)

except serial.SerialException, e:

print "Could not open serial port: %s" % e

sys.exit(1)

dev = slcd.SLCD(port)

Python SLCD Driver

class SLCDException(Exception):

"""Base class for SLCD related Exceptions"""

class SLCD:

def __init__(self, serial):

"""Create the SLCD communication device"""

self.serial = serial

self.initial_baudrate = self.serial.getBaudrate()

self.last_cmd = ""

self.synchronize()

self.inventory_loaded = False

self.response = ""

Communication Protocol

SLCD

Command input operates line-at-a-time

Example: t “Hello World” 50 100\r

Returns “>\r” when prior command okay

Returns “!\r” when prior command failed

CMUcam2

Command input operates line-at-a-time

Example: sv 0 120\r

Returns “ACK\r:” when prior command okay

Returns “NACK\r:” when prior command failed

Python SLCD Protocol

# wait for ">" or "!" prompt

def waitCmd(self):

"""Wait for ACK (> or !) from SLCD"""

data = self.serial.readline(eol='\r')

if data == ">\r":

return True

elif data == "!\r":

return False

else:

if data == "":

raise SLCDException("'%s' timed out" \

% self.last_cmd)

raise SLCDException("'%s' responded with " \ “unexpected characters: '%s'" % \

(self.last_cmd, data))

SLCD Synchronization

def synchronize(self):

"""Synchronize with line-at-a-time protocol"""

self.serial.flushInput()

self.serial.write("\r")

retry = 0

okay = False

while not okay and retry < 3:

try:

okay = self.waitCmd()

except SLCDException, e:

time.sleep(1)

self.serial.flushInput()

self.serial.write("\r")

retry+=1

if not okay:

raise SLCDException("sync failed " \

"after '%s' command" \

% self.last_cmd)

# TODO: try various baudrates when failing

return okay

SLCD Command Processing

def cmd_issue(self, s):

"""Issue a command, not waiting for ACK"""

self.response = ""

self.last_cmd = s

self.serial.write(self.last_cmd)

self.serial.write("\r")

return True

def cmd(self, s):

"""Issues a command and waits for response"""

self.cmd_issue(s)

return self.waitCmd()

SLCD Command Definitions

def set_rgb(self,fore,back):

"""Change the default drawing colors"""

return self.cmd("S %s %s" % (fore, back))

def bitmap(self,index,x,y):

"""Display Bitmap"""

return self.cmd("xi %d %d %d" % (index,x,y))

def reset(self):

"""Software reset""" self.cmd_issue("*RESET")

# Need to let baud rate settle

time.sleep(1)

self.serial.setBaudrate(self.initial_baudrate)

return self.synchronize()

Unit Testing

Embedded device tests

Syntax validation

Non-interactive

Interactive

PyUnit

http://pyunit.sourceforge.net/

Very easy to use

PyUnit Methods

unittest.TestCase

setUp()

failUnless(expr, msg=None)

test_*

unittest.TestSuite

Initialized as a collection of TestCases

unittest.TextTestRunner

run()

PyUnit TestCase

class TestSLCD(unittest.TestCase):

def setUp(self):

"""Sync device input for each new test"""

dev.synchronize()

def test_cmd_reset(self):

"""Test the "*RESET: software reset" command"""

self.failUnless(dev.reset(),"Reset command")

self.failUnless(dev.version(),"Version command")

# TODO: find some state to set and then check

def test_cmd_baud(self):

"""Test the "baud: set baud rate" command"""

for rate in (9600,19200,38400,57600,115200):

dev.text("Trying baud %d ... " % rate)

self.failUnless(dev.baud(rate), \

"Baud rate failed: %d" % rate)

self.failUnless(dev.version(), \

"Version after baud change failed")

self.failUnless(dev.reset(), \

"Reset command failed")

PyUnit TestSuite

class TestSLCDSuite(unittest.TestSuite):

def __init__(self):

"""Create an ordered list of tests for perform"""

unittest.TestSuite.__init__(self,

map(TestSLCD,

(

# Verify we can communicate

"test_cmd_reset",

# Verify all baud rates

"test_cmd_baud",

)))

dev.reset()

unittest.TextTestRunner(verbosity=2).run(TestSLCDSuite())

PyUnit TestRunner

dev.reset()

unittest.TextTestRunner(verbosity=2).run(TestSLCDSuite())

PyUnit In Action

$ python test-slcd.py

Test the “*RESET: software reset” command ... ok

Test the “baud: set baud rate” command ... ok

---------------------------------------------------

Ran 2 tests in 6.003s

OK

$ python test-slcd.py

Traceback (most recent call last):

File "test-slcd.py", line 32, in ?

dev = slcd.SLCD(port)

File "slcd.py", line 14, in __init__

self.synchronize()

File "slcd.py", line 34, in synchronize

raise SLCDException("sync failed after '%s' command" \

% self.last_cmd)

slcd.SLCDException: sync failed after '' command

Questions

If I run out of time, or you're too shy, send me email or find me on IRC!

work: kees@osdl.org

home: kees@outflux.net

IRC: keescook on FreeNode

Watch out, my web pages a bit out of date.

Kees Cook

2005-05-25T11:48:44