Endurance testing with python

From RidgeRun Developer Wiki


Overview

The following information covers some of the challanges when doing power cycle endurance testing where the unit under test (UUT) is connected to the python controlling application using a serial port. Some of the challenges include not letting the extranous data that is often received over serial as the UUT powers off and back on. These characters can trip errors in the python Unicode decoder that need to be ignored.

Links

Power cycle setup

The key components involved include:

The only downside I have with X10 is you can't turn it off and back on quickly - something on the order of 3 seconds minimum. I like X10 because I have lots of applicance modules so I can run multiple tests at the same time, I can use it with desktop icons so I don't have to physically power cycle hardware, and because the appliance module can be connected to any outlet, I can set the endurance test hardware out of the way.

python X10 control

There are three subtle host PC configuration steps that I found to be needed to allow X10 power control to work smoothly. Hopefully future Linux distros will do a better job supporting X10 modules.

  • Unplug CM19a X10 Transceiver
  • Black list other drivers that attempt to own the X10 USB RF tranceiver
echo -e "#Blacklist to enable CM19a driver\nblacklist lirc_ati\nblacklist ati_remote" | \
        sudo tee -a /etc/modprobe.d/blacklist.conf
sudo modprobe --remove lirc_atiusb ati_remote
  • Allow all users to interact with the X10 USB RF tranceiver
echo -e "# Allow all users to read and write to the CM19a X10 Transceiver (USB)\nSYSFS{idVendor}==\"0bc7\", SYSFS{idProduct}==\"0002\", MODE=\"666\"" | \
        sudo tee /etc/udev/rules.d/cm19a.rules
  • Plug in the X10 USB RF tranceiver
  • Remove old version of python USB module
sudo apt-get remove python-usb

Install updated python USB module

wget http://softlayer-dal.dl.sourceforge.net/project/pyusb/PyUSB%201.0/1.0.0-alpha-3/pyusb-1.0.0a3.tar.gz
tar -xzf pyusb-1.0.0a3.tar.gz
cd pyusb-1.0.0a3/
python setup.py build
sudo python setup.py install
  • Unfortunately, the X10 USB RF tranceiver python code doesn't come with the standard setup.py, so I ended up installing it in the same directory tree as my python expect scripts resided.
mkdir $HOME/work/pexpect
cd $HOME/work/pexpect
wget http://ubuntuone.com/0O4fjPSt2QFAY8yNcdxZVz
unzip CM19aDriver_v3.0.zip

Now you can test by turning a module on and off. My module is set to house code G unit code 3.

./CM19aDriver.py G 3 ON
./CM19aDriver.py G 3 OFF

python expect setup

The good news is the is an expect package for python called pexpect. If you have used any of the other variations of expect, you will find the python version works as you would expect.

I added my Linux user ID to the dialout group so I could send and receive data or the serial ports and also installed the pexpect package.

sudo addgroup $USER dialout
sudo apt-get python-pexpect

Ignoring binary data

The bad news is I found pexpect to assume it was working with a text (not binary) data stream. When binary data was received, the python exception was so low that I couldn't figure out how to easily ignore it while waiting for the expected data. In my case, the binary data was due to powering off and on devices connected over RS-232 serial. Occatiionally I would get a random binary pattern during a power cycle and python said it could decode the Unicode character. Thanks to Open Source code, I could at least work around the issue.

I ended up changing the pexpect 2.5.1 source code to solve this issue. Specifically, I made the following changes to /usr/local/lib/python2.7/dist-packages/pexpect/__init__.py. The changes basically told python to ignore unicode decode error and to strip non-ASCII from strings before logging them.

--- pexpect-u-2.5.1/pexpect/__init__.py	2011-12-10 15:48:07.000000000 -0700
+++ ../pexpect__init__.py	2013-10-01 11:13:59.218765248 -0600
@@ -152,7 +152,7 @@
 
 def _cast_unicode(s, enc):
     if isinstance(s, bytes):
-        return s.decode(enc)
+        return s.decode(enc, 'ignore')
     return s
 
 re_type = type(re.compile(''))
@@ -864,7 +864,7 @@
                 self.flag_eof = True
                 raise EOF ('End Of File (EOF) in read_nonblocking(). Empty string style platform.')
 
-            s2 = self._cast_buffer_type(s)
+            s2 = "".join(i for i in self._cast_buffer_type(s) if ord(i)<128)
             if self.logfile is not None:
                 self.logfile.write(s2)
                 self.logfile.flush()
@@ -1620,12 +1620,12 @@
     def _prepare_regex_pattern(self, p):
         "Recompile bytes regexes as unicode regexes."
         if isinstance(p.pattern, bytes):
-            p = re.compile(p.pattern.decode(self.encoding), p.flags)
+            p = re.compile(p.pattern.decode(self.encoding, 'ignore'), p.flags)
         return p
     
     def read_nonblocking(self, size=1, timeout=-1):
         return super(spawn, self).read_nonblocking(size=size, timeout=timeout)\
-                                    .decode(self.encoding)
+                                    .decode(self.encoding, 'ignore')
     
     read_nonblocking.__doc__ = spawnb.read_nonblocking.__doc__

In looking at the latest pexpect code at git@gitorious.org:pexpect/pexpect.git, there is no direct call to decode(), but they logging change may still be useful.

python endurance testing example

# There are two parts to this code - one is the X10 power switch logic
# to power the board on and off.  The other is the expect logic to monitor
# the boot process.

# To configure the port, first run
# stty -F /dev/ttyUSB5 115200 cs8 -clocal -crtscts -parenb -inpck -ixoff -ixon

import os, sys, time
from datetime import datetime

# System dependent constants

cm19adriverdir='/projects/x10'
#devport="/dev/ttyUSB5"
prompt = "# "
rootpassword="foobar"
x10housecode="G"
x10unitcode="3"
# delay in seconds
poweroffdelay=5
# set bootlog to sys.stdout or file("boot.log", "a")
bootlog=file("boot.log", "a")

import pexpect
import fdpexpect

homedir=os.environ['HOME']
sys.path.append(homedir + cm19adriverdir)
import CM19aDriver

# Initialize power switch
cm19a = CM19aDriver.CM19aDevice()

banner=time.strftime("---------------%Y-%m-%d %H:%M:%S---------------\n", time.localtime())
bootlog.write(banner)
print banner

# returns number of errors encountered (zero or one)
def wait_for(m, msg,tm=60) :
    try:
        m.expect(msg,timeout=tm)
        return 0
    except pexpect.TIMEOUT :
        return 1
    except pexpect.EOF :
        # sometimes the /dev/tty* UART port reports EOF, just ignore
        return 0

def init_expect() :
    tm = int(time.mktime(time.localtime()))
    btlog = file("boot-" + repr(tm) + ".log", "w")

    fd = os.open(devport, os.O_RDWR|os.O_NOCTTY )
    m = fdpexpect.fdspawn(fd, timeout=60, logfile=btlog)
    m.setecho(False)
    return m

def power_on(m) :
    result = cm19a.send(x10housecode, x10unitcode, "ON") 

    try:
        # verify we get init running
        wait_for(m, " login: ", tm=180)
        m.sendline("root")
        wait_for(m, "Password: ")
        m.sendline(rootpassword)
        wait_for(m, ":~# ")

    except (pexpect.EOF, OSError) :
        print "Serial port error"
        result = cm19a.send(x10housecode, x10unitcode, "OFF") 
        return 1
    return 0

def power_off(m) :
    cm19a.send(x10housecode, x10unitcode, "OFF") 

def run_remote_command(m, cmd) :
    m.sendline(cmd);
    wait_for(m, cmd)
    wait_for(m, prompt)
    print m.before.strip()

def endurance_test_power_cycle() :
    loops = 0
    m = init_expect()

    while True :
        try:
            print "LOOP COUNT %d" % loops
            power_on(m)
            time.sleep(poweroffdelay)
            power_off(m)
            loops += 1

        except KeyboardInterrupt : 
            print
            print "Keyboard interrupt:"
            print "  LOOP COUNT %d" % loops
            cm19a.send(x10housecode, x10unitcode, "OFF") 
            return

        except (pexpect.EOF, OSError) :
            # sometimes the /dev/tty* UART port reports EOF, just ignore
            print "uart error:"

if __name__ == '__main__' :
    # get to known state
    result = cm19a.send(x10housecode, x10unitcode, "OFF") 
    time.sleep(poweroffdelay)

    endurance_test_power_cycle

TP Link HS100 HS101

The GadgetReactor pyHS100 python3 Open Source project provides a nice CLI tool. I fought problems with PYTHONPATH to get pyhs100 to work on my Mac.

First, configure your HS100 using KasaSmart phone app. Once the HS100 is able to connect to your wifi access point, you can use pyhs100 to automate control.

export PYTHONPATH=
pip3 install pyHS100
pyhs100 

The pyhs100 CLI will search for your HS100. I got the output:

No host name given, trying discovery..
Discovering devices for 3 seconds
== TP Switch - HS100(US) ==
OFF
Host/IP: 10.111.0.71
LED state: True
On since: 2019-09-13 11:18:18.256648
== Generic information ==
Time:         2019-09-13 11:18:18
Hardware:     1.0
Software:     1.1.1 Build 160725 Rel.163650
MAC (rssi):   50:C7:BF:9B:AC:C7 (-68)

Then you can control the switch:

pyhs100 --host 10.111.0.71 --plug on
pyhs100 --host 10.111.0.71 --plug off

It took 20 seconds to turn the plug on and off 100 times. there was an odd pause after every 6 power cycles.

Old stuff to be ignored

  • Unfortunately, the X10 USB RF tranceiver python code doesn't come with the standard setup.py, so installing the module is a manual process
wget http://ubuntuone.com/0O4fjPSt2QFAY8yNcdxZVz
unzip CM19aDriver_v3.0.zip
cat <<EOF >setup.py
#!/usr/bin/env python

from distutils.core import setup

setup(
    name='cm19a',
    version='3.0',
    description='Python CM19A X10 USB RF tranceiver controller',
    author='Andrew Cuddon',
    author_email='andrew@cuddon.net',
    license = 'Unknown',
    url='http://www.cm19a.com/2013/02/python-x10-cm19a-usb-software-linux.html'
    packages=['cm19a'],
    long_description =
"""
CM19aDriver offers easy CM19A X10 USB tranceiver control in Python.
It requires a newer version of pyusb.
"""
)
EOF
python setup.py build
sudo python setup.py install