Chapter 10. Building and Using Simulators

Is that some kind of a game you are playing?

C. A. Chung, Simulation Modeling Handbook: A Practical Approach

So far in this book we’ve covered the basics of programming in Python, reviewed some essential electronics, and explored the tip of the iceberg of control systems theory. We’ve covered a lot, to be sure, but there is still one major topic left before we take on the challenges of actually connecting a computer to an instrument or a control system and turning it loose: simulation.

In engineering, simulation can be applied to many things, from a simple device to an entire complex system. In electronics engineering, circuit simulations are used to explore and analyze analog and digital designs well before an IC is fabricated or a soldering iron comes into play. Systems engineers build complex simulations of industrial systems to evaluate various control strategies and process flow models long before the pipes are laid out and the conveyors are installed. Military and commercial pilots are trained in realistic aircraft simulators where procedures and techniques can be learned and practiced with no risk to an actual vehicle or the people in it (or on the ground).

The primary objective of this chapter is to equip you with extensible simulation tools that you can reuse in other projects later, as well as an understanding of when and where simulation is useful, and where it is not. To this end, we’ll examine a couple of complete simulators written entirely in Python. We’ll wrap up by looking at some ways to leverage commonly available (and free) software tools to create other simulators.

The first example we will consider is a simulation of a generic multifunction device with both analog and discrete I/O. The second simulator example is an eight-channel AC power controller. Although the simulators in this chapter will touch on topics such as data I/O, data capture, and user interfaces, we will defer in-depth discussions of those topics to later chapters. Chapter 11 will examine data I/O in detail, and in Chapter 12 we will look at some ways to load and save data using files. In Chapter 13 we’ll explore user interfaces in more detail, including the TkInter and wxPython GUIs. It is my hope that you will take the initiative to return to these simulators and extend their usefulness with the knowledge you gain later.

What Is Simulation?

If you’ve ever played a video game, you’ve used a simulator. One of the first commercially successful video games, Atari’s Pong, was a simulation (albeit crude by today’s standards) of a ping-pong game. In fact, all video games are simulations of something—what they simulate might not actually exist in the real world, but they’re still simulations. By the same token, a simulation of a control system that doesn’t really exist allows us to try out different novel ideas, invent worst-case scenarios to evaluate system behavior, and explore various behavioral models, all without risking any hardware or jeopardizing personal safety.

The key concept of simulation is that all simulators are based on a model of some sort. Models can be simple, or they can be complex. A model may be event- or time-series-based (automated handling of luggage at a large airport), purely mathematical (optical performance of lenses and mirrors), or some combination of these and other factors. One way to think of the core model in a simulation is as a dynamic virtual system. If, for example, you have a high-fidelity simulator for some type of chemical processing system, there is, in essence, a virtual chemical processing system in the simulation software that will exhibit as many of the responses and characteristics of the real thing as the fidelity of the simulation will allow. A truly high-fidelity simulation might even produce a simulated chemical product.

Figure 10-1 shows how a simulation corresponds to its real-world counterpart. The instrumentation system (which would typically be what we are developing or testing in this book) uses a simulated interface to interact with a model. The model allows us to observe and analyze how the instrumentation software will behave when connected to a system (in this case, a virtual system). We can implement a crude simulation model with basic behavior, or we could build something that has a very high degree of fidelity. We can also inject simulated faults into the model and examine the response of the instrumentation system.

Simulation versus the real world
Figure 10-1. Simulation versus the real world

When implementing data acquisition and control systems, simulations of the devices connected to the control PC can be used to speed up the development process and provide a safe environment to test out ideas. Simulation can also provide some invaluable, and otherwise unattainable, insights into the behavior of the instrumentation software and the device or system being simulated. Whether it’s implemented because the instrumentation hardware just isn’t available yet or because the target system hardware is too valuable to risk damaging, a simulation is a good way to get the software running, test it, and have a high degree of confidence that it will work correctly in the real world.

Low Fidelity or High Fidelity

When talking about simulation, one of the first considerations to come up is the issue of fidelity. The fidelity of a simulation defines how accurately it will model a real system. The cost and effort of implementing a simulation can rise significantly with each increase in the level of fidelity, so you’ll have to decide when it’s good enough and resist the temptation to polish it up too much.

A common error made when attempting to write a simulator for the first time is to throw everything into it. Even seasoned pros with access to lots of real data from a real system don’t usually do this. There are too many unknowns at the outset. Subtle behavioral interactions can be surprising and might never have been seen in a real system, and assumptions made about the more opaque parts of a system may contain all sorts of traps and pitfalls.

This is why simulations are typically built up incrementally. First, the software interface, or API, is defined. A generic interface may use different names for the API functions, but it should accept and return data that looks as much like real API data as possible. In other words, the simulation should support the basic algorithmic functionality of the control or instrumentation system you are building in terms of the data that needs to pass through the interface. Once the interface simulation is at a point where it can support initial testing and development, simulation fidelity can be improved as necessary.

That being said, sometimes a low-fidelity simulator consisting of just an API will work fine. For example, if all you really want to do is verify that a proportional control function is working correctly, a simple data source to drive the simulation and some way to capture and save the output may be all that’s necessary.

Simulating Errors and Faults

In addition to simulating the functionality of a working system, a simulation may also need to have the ability to simulate an error. In other words, it should be able to simulate being broken in some predefined way.

If you’ve done a failure analysis, as outlined in Chapter 8, you should have an idea of what types of errors might occur and how they will cause the system to fail. The ability to simulate those errors allows you to see how well your software will deal with them.

Note

If you do want to include the ability to do fault injection in a simulation, a preliminary failure analysis is an essential first step.

In general terms, there are two primary classes of faults (not including bugs that may be lurking in the instrumentation code itself) that come into play: interface faults and system faults. The line between these two classes of faults isn’t always distinct, but in a simulated environment it’s usually possible to treat them as separate classes. This, in turn, makes it much easier to clearly distinguish cause and effect without the complexity and messiness of dealing with real physical components.

Interface faults

An interface fault results in an error that, as the name implies, occurs in the API layer between the system under development or in test, and the system simulation behind it. These types of faults might manifest as communications errors, such as corrupt data or no response. In other words, an interface fault occurs in between the instrumentation software and the simulated system. In the real system, errors in the interface might arise as a result of broken wires, corroded connections, a defective electronic component, or spurious noise.

Simulating interface faults typically involves things like disabling a communications channel or injecting random data into a channel. You can also simulate a fault in a piece of bus interface hardware (e.g., a plug-in I/O card), although this falls into that gray area I alluded to earlier and may be better dealt with as a system fault.

Figure 10-2 shows a diagram of an interface simulation with basic fault injection capabilities. Notice that the TxD (transmit) output from the instrumentation software to the instrument simulator doesn’t have a disable capability. This isn’t an oversight. If the instrument employs a command-response type of control interface, disabling the TxD link between it and the instrumentation software doesn’t really accomplish much. The instrument (or simulated instrument) should just sit and wait for a command, and it won’t have any way to detect whether the link is broken or not.

Interface fault injection
Figure 10-2. Interface fault injection

The path from the instrument to the instrumentation software is another matter. In this case, the instrumentation software should be able to detect when it has not received a response from the instrument and react in a predefined manner.

A reasonable question to ask at this point would be: why not just simulate the disabled communications in the instrument simulator, instead of using an interface simulation? The reason is that the instrument, or control system, may interact with some other component in the system that the instrumentation software can sense even if the primary link to the instrument is disabled. In other words, clever instrumentation software can sometimes get verification from another source (e.g., sensing a power state change) indicating that a correct command action has occurred, and thereby determine that the interface is the probable cause of the problem.

Figure 10-2 also indicates that both the TxD and RxD (receive) channels have noise injection capabilities. In the context of a communications channel that handles streams of ASCII characters, this could involve the injection of random characters to simulate line noise. Anyone who has ever had to use a modem to communicate with a remote computer system has mostly likely seen examples of line noise when the modem connects to or disconnects from the remote host.

Instrumentation software that is able to deal with corrupted data will most likely have the ability to retry an operation, or at least issue a query to determine whether a command was accepted correctly. It is also possible to determine where the noise occurred by examining the response from the instrument. If the instrument complains that it received an invalid command, the noise occurred on the TxD channel from the instrumentation software. If the response from the instrument is garbage, the noise is most likely in the RxD channel to the instrumentation software.

System faults

A system fault is something that arises from within the external system, or perhaps one of its subsystems (depending on the complexity of the system). The ability to simulate various system faults increases the value of the simulation, allowing you to exercise and verify the fault response of the instrumentation software while interfaced with the simulation.

A system fault can be any number of things, but in a simulator it usually comes down to a value in a table or the return code from a function. In other words, if the simulator would normally return an expected value in response to a particular input, simulating a fault is simply a matter of including some logic to return an invalid value or an error indication instead.

One approach is to directly inject the fault at the return point of a function or method in the simulation, as shown in Figure 10-3.

System-level fault injection
Figure 10-3. System-level fault injection

Assuming that ret_val in Figure 10-3 holds the nominal value that the function or method would normally return, setting simerr to True will result in the error return value (err_val) being passed back to the caller instead. The error value could be set via an accessor method, loaded from a fault table, or even entered manually from a user interface of some sort.

Another approach is to establish a fault condition using a module global variable or an object variable, as shown in Figure 10-4.

Simulator fault injection using object variables
Figure 10-4. Simulator fault injection using object variables

In Figure 10-4, the class definition for SimObject has been designed to specifically expose key variables used by the methods in the class. In a simplistic scenario these would be read-only variables, set once at object instantiation and not modified by the class methods. It is possible to implement a lockout capability to prevent methods from modifying shared object variables, but that usually entails a level of complexity that’s not always necessary.

If you’re experienced with unit testing techniques, you might be thinking that all this sounds rather familiar, and you would be correct (we also discussed this in Chapter 8). In unit testing, a primary objective is to exercise all possible paths of execution to achieve complete statement coverage. If the unit under test has paths that can be executed only when an error occurs, the unit test environment must be able to set variables or coerce inputs to stimulate the appropriate error responses. A simulation that has fault injection capabilities does much the same thing, although for a somewhat different reason.

As a test environment, a simulator can be used to create fault scenarios that would otherwise be difficult (or even near impossible) to replicate with real hardware, at least not without the possibility of risking some serious (and expensive) physical damage. Simulation with fault injection allows you to observe the behavior of the control or data acquisition system you are building to see how it will respond to various error conditions and, if you have been following some requirements that define error responses, to determine whether it meets those requirements.

Using Python to Create a Simulator

In this section we will examine two complete simulators written in Python: DevSim and the Simple Power Controller (SPC). Later, we’ll look at some other ways to achieve simulation using free and open source software. The sources for the simulators are located in the resource repository for this book, which can be accessed from the book’s web page. While these are fully functional simulators, they are also only examples, not production-grade tools. My hope is that you will use them as inspiration, or perhaps as starting points, to create your own simulator tools to meet your particular needs.

Python is well suited to creating simulators. It is easy to use, very flexible, and allows you to easily implement things such as plug-in modules. Working simulators can often be gotten up and running very quickly, and once in place they can be readily extended and revised as necessary. In addition, Python is capable of some impressive math tricks when add-on libraries such as SciPy or NumPy are installed, and as we’ll see shortly, generating graphical output is not that difficult.

On the other hand, Python is not as fast as code written in C or C++. That’s just the nature of the language and its underlying interpreter. If you need high-speed data generation and fast responses, you should probably consider another approach. Fortunately, most instrumentation applications have rather long time constants to begin with, so speed is usually not an issue.

Package and Module Organization

The simulators we’ll examine in this chapter are DevSim and SPC. Each can reside in its own package (subdirectory). This is how I’ve arranged them, with SPC residing in the ACSim directory. DevSim imports the FileUtils module that we’ll see in Chapter 12 for reading and writing ASCII data files. It also imports a module called RetCodes, which contains a set of pseudoconstants for return code values. It is intended to be a read-only shared file. Typically the FileUtils and RetCodes modules would reside in a separate package called SimLib, as shown in Figure 10-5, along with any other modules that your simulators and utilities might need to share.

Simulation package structure
Figure 10-5. Simulation package structure

If you have not already done so, now would be a good time to pause and download the source code for this book from its website. There’s more there than what you’ll see here, and it’s all already neatly organized with installation instructions.

Notice that the top-level package, PySims, has a file called __init__.py. This establishes PySims as the “anchor” of the package hierarchy; it may contain a docstring, package initialization code, or nothing at all. When we look at gnuplot later, we’ll see how one developer elected to use __init__.py to handle package initialization.

Data I/O Simulator

First up is a rather substantial simulator that is intended to stand in for a bus-based multifunction I/O card. Right out of the box it can be used with the PID control code we saw in Chapter 9 (and I’ll show you how to do that in just a bit). It also has the ability to incorporate user-defined functions as part of the response data processing, which is useful for simulating things like mechanical inertia or applying a filter function.

DevSim internals

Although the DevSim API may look daunting, it’s mainly just a collection of accessor methods to set simulation parameters and return the current values. In its basic form, it doesn’t actually simulate any particular external device or system. That functionality is added by you, in the form of user-definable functions or additional software to meet project-specific needs.

Rather than list it all here (it’s almost 1,000 lines of code), I’m going to describe its internal structure, list the various methods available, show some highlights, and discuss how the simulator is used. You should have the source listing handy for reference, if possible.

Internally, DevSim is the most complex thing we’ve seen so far, but keep in mind that it’s mostly just data routing. Figure 10-6 is an IC data-sheet-style logic diagram for DevSim.

Data is buffered as it moves though DevSim, and each activity is synchronized so as to occur in a lock-step fashion. DevSim is also multithreaded: four threads handle cyclic functions (waveform generation), another four manage file input, and a ninth thread handles the simulator’s primary sequencing in a main loop.

DevSim internal logic
Figure 10-6. DevSim internal logic

The DevSim class’s __init__() method presets the internal parameters to default values, and then calls the internal method __run() as its final action. The code for __run() follows:

#-----------------------------------------------------------------
# Simulator launch
#-----------------------------------------------------------------
def __run(self):
    """ Simulator start.

        Instantiates the main loop thread, sets up the cyclic and
        file I/O threads, and then waits for the start flag to go
        True. When the start flag becomes True the main loop
        thread is started.
    """
    # input thread object lists
    cycThread  = [None, None, None, None]
    fileThread = [None, None, None, None]

    simloop  = threading.Thread(target=self.__simLoop)

    # create the cyclic and file input handling threads,
    # four of each
    for inch in range(0,4):
        cycThread[inch]  = 
            threading.Thread(target=self.__cyclic, args=[inch])
        fileThread[inch] = 
            threading.Thread(target=self.__fileData, args=[inch])

        # start the cyclic and file I/O threads just created
        cycThread[inch].start()
        fileThread[inch].start()

    # wait for start signal then start the main thread
    wait_start = True
    while wait_start:
        if self.startSim == True:
            wait_start = False
        else:
            time.sleep(0.1)     # wait 100 ms
    simloop.start()             # start it up

First, the primary thread, simloop(), is created. Next, the eight input threads are instantiated in a pair of lists. As each thread is created, it is subsequently started. The __run() method then waits for a start signal, and finally starts the simloop() main thread.

The simulator is designed to start running when a Boolean variable (startSim) is set to True. You can set the various parameters before enabling the simulator, or they can be set “on the fly” while it is running. The simulation is stopped by setting the value of the Boolean object variable stopSim to True.

The four cyclic threads and the four file input threads each load data into a buffer when an “event” occurs. In the case of DevSim, an event is simply a flag variable that is set and reset to control activity; it is not a true event in the sense of a low-level message such as one would find in a GUI. Figure 10-7 shows how the threads are used to obtain input data and place it into buffers for eventual routing to the outputs. Notice that the __cyclic() threads can each select from one of five possible inputs: DC (constant value), sine wave, pulse (square wave), ramp, and sawtooth. The cyclic data is updated at a rate set by the parameter accessor setCyclicRate(), and each cyclic input may have a unique rate value. The cyclic inputs are very useful for generating a fixed DC-like value or periodic waveforms that the instrumentation software connected to the simulator can read and process.

Cyclic and file input threads
Figure 10-7. Cyclic and file input threads

Using threads for the cyclic inputs allows them to run at different rates than the simulator’s main thread, and this in turns allows them to create waveform data at specific frequencies. If they weren’t implemented as threads they would generate changes in the simulated waveforms at the main loop rate of the simulator, which wouldn’t be suitable for a lot of situations. Allowing the cyclic data generators to run asynchronously is an example of timing decoupling. The buffers are necessary to capture the asynchronous data as it is generated.

The __fileData() threads are used to obtain data values from a file and pass them through to the instrument software under test. This feature allows you to replay a set of input data over and over again. It is useful when evaluating the instrumentation software under test against a known set of input values, to observe its behavior.

The main loop thread, shown next, is where all the data is collected and routed to the output buffers:

def __simLoop(self):
    """ Simulator main loop.

        The main loop continuously checks for input data (either
        from an external caller or from a cyclic source) for each
        input channel. If data is available it starts the
        processing chain that will ultimately result in the data
        appearing in the output buffers.

        The main loop runs forever as a thread. All externally
        supplied data is buffered, and all output data is buffered.
        When running in cyclic mode the output buffers will be
        overwritten with new data as it becomes available.
    """
    while self.stopSim != True:
        # scan through all four input channels, get available data
        for ichan in range(0, 4):
            if self.in_src[ichan] == DS.EXT_IN:
                indata = self.inbuffer[ichan]
            else:
                indata = __getCycData(ichan)

            # if a user-supplied function is defined, then apply
            # it to the data
            self.databuffer[ichan] = self.__doUserFunc(ichan, indata)

        # fetch file data (if configured to do so)
        self.__getFileData(ichan)

        # step through each output channel, move data as nesc
        for ochan in range(0,4):
            if (self.out_src[ochan] >= DS.INCHAN1) and 
               (self.out_src[ochan] <= DS.INCHAN4):
                outdata = self.databuffer[ochan]
            else:
                outdata = self.filebuffer[ochan]

            randdata = self.__getRandom()

            oscaled = self.__scaleData(outdata, self.outscale[ochan])
            rscaled = self.__scaleData(randdata, self.randscale[ochan])

            self.outbuffer[ochan] = oscaled + rscaled
            self.outavail[ochan] = True

        time.sleep(self.simtime)

Reading through the code for the __simloop() method is equivalent to following the data flow through Figure 10-6 from left to right. The first step is to get either externally supplied data (via the sendData() method, described in the following section), or from one of the cyclic data sources. Next the data is passed to a user-defined function, if one exists. It is then written to a buffer (databuffer).

If the data input is a file, the data is read from the file and stored in another buffer. Note that although a method is called, this doesn’t mean that anything will actually happen. An input method might be quiescent and therefore skipped internally, depending on how the simulator is configured.

Once the input data is in one of the two input buffers, it is read out and passed to the output. Along the way random data is obtained (for noise simulation), and the data is optionally scaled before it is summed with scaled random data. Finally, the data ends up in the output buffers for each channel.

DevSim methods

The DevSim class contains 26 public methods and 15 internal methods. Most of the public methods are devoted to setting or retrieving internal parameter values. The public methods are described in the following lists.

Note

When used, the parameters inchan and outchan may be any channel number from 0 to 3.

Parameter accessor methods:

getCyclicLevel(inchan)

Returns a 2-tuple with either NO_ERR and the current cyclic level of the specified channel, or BAD_PARAM and None if inchan is out of range.

setCyclicLevel(inchan, level)

Sets the output value for a cyclic source in CYCNONE mode for a specific input channel. Returns NO_ERR if successful or BAD_PARAM if inchan is out of range.

getCyclicOffset()

Returns the current cyclic offset value.

setCyclicOffset(offset)

Sets the current cyclic offset value for all cyclic data. The offset is the shift of the peak-to-peak range of the output relative to zero, otherwise known as the bias.

getCyclicRate(inchan)

Returns a 2-tuple with either NO_ERR and the current cyclic rate of the specified channel, or BAD_PARAM and None.

setCyclicRate(inchan, rate)

The parameter rate defines the cyclic rate in fractional seconds for a specific input channel. Note that this is the period of the cyclic data, not the frequency. The frequency is the inverse of the rate. Returns NO_ERR if successful or BAD_PARAM if inchan is out of range.

getCyclicType(inchan)

Returns a 2-tuple with either NO_ERR and the current cyclic wave shape of the specified channel, or BAD_PARAM and None.

setCyclicType(inchan, cyctype)

Defines the output wave shape of a cyclic data source. The available wave shapes are sine, pulse, ramp, and sawtooth. A cyclic source may also be set to generate a constant output value, and the value may be changed at any time while the simulator is active. This, in effect, emulates a variable voltage source. Table 10-1 lists the five cyclic data types available.

Table 10-1. Simulator cyclic data types

Cyclic type

Data value

Description

CYCNONE

0

Constant output level

CYCSINE

1

Sine wave

CYCPULSE

2

50% duty-cycle pulse (i.e., a square wave)

CYCRAMP

3

Ramp wave shape with leading slope

CYCSAW

4

Sawtooth wave with symmetrical rise/fall

Returns NO_ERR if successful or BAD_PARAM if cyctype is invalid or inchan is out of range.

setDataFile(infile, path, filename, recycle=True)

Defines and opens a data file for input as a data source specified by the infile index. If path is not specified (empty string or None), the default path is assumed to be the current working directory. The input file must contain data in one of the four formats supported by the ASCIIDataRead class in the module FileUtils.

If the parameter recycle is True, the file will be reset to the start and reread when an EOF is encountered. The default behavior is to recycle the data file. If a data source file is already opened for a given input channel and this method is called, the currently open file will be closed and the new file will be opened.

Returns OPEN_ERR if the file open failed or BAD_PARAM if infile is invalid; otherwise, returns NO_ERR.

getDataScale(outchan)

Returns a 2-tuple with either NO_ERR and the current data scaling for a specific channel, or BAD_PARAM and None.

setDataScale(outchan, scale)

Sets the output channel’s input data scaling factor. Each output channel may have an optional unique multiplicative scaling factor applied. Returns NO_ERR if successful or BAD_PARAM if inchan is out of range.

getFunction(inchan)

Returns a 2-tuple with either NO_ERR and the current function string for the specified channel, or BAD_PARAM and None.

setFunction(inchan, funcstr=' ')

Applies a user-supplied function expression to the data stream of a specified input data channel using two predefined variables:

x0

Input data

x1

Previous (1/z) data

The function is a string. It may reference the x0 and x1 variables but may not contain an equals sign (an assignment). The result is used as the data input to the output channels. Passing None or an empty string disables the application of a function to the data. Returns NO_ERR if successful or BAD_PARAM if inchan is out of range.

getInputSrc(inchan)

Returns a 2-tuple with either NO_ERR and the input source for a specific channel, or BAD_PARAM and None.

setInputSrc(inchan, source)

Selects the data source for an input channel. inchan may be any valid input channel number from 0 to 3. The source parameter may be one of EXT_IN (default) or CYCLIC. The input source may be changed on the fly at any time. Returns NO_ERR if successful or BAD_PARAM if inchan is out of range or source is invalid.

getOutputDest(outchan)

Returns a 2-tuple with either NO_ERR and the current output channel data source, or BAD_PARAM and None.

setOutputDest(outchan, source)

Selects the data source for an output channel. outchan may be any valid output channel number between 0 and 3. The source parameter may be one of INCHAN1, INCHAN2, INCHAN3, INCHAN4, SRCFILE1, SRCFILE2, SRCFILE3, or SRCFILE4. Returns NO_ERR if successful or BAD_PARAM if inchan is out of range or source is invalid.

getRandScale(outchan)

Returns a 2-tuple with either NO_ERR and the current random data scaling multiplier, or BAD_PARAM and None.

setRandScale(outchan, scale)

Sets the output channel’s random data scaling factor. Each output channel may have an optional multiplicative scaling factor applied to the random data. If the scaling is set to zero, no random values are summed into the data. Returns NO_ERR if successful or BAD_PARAM if inchan is out of range.

getSimTime()

Returns the current simulator cycle time.

setSimTime(time)

Sets the overall cycle time of the simulation. This is, in effect, the amount of time for which the main loop will be suspended between each loop iteration. The time is specified in fractional seconds. Returns nothing.

getTrigMode(inchan)

Returns a 2-tuple with either NO_ERR and the current trigger mode for the specified channel, or BAD_PARAM and None.

setTriggerMode(inchan, mode)

Sets the trigger mode for a particular channel. The trigger mode may be one of NO_TRIG (0), EXT_TRIG (1), or INT_TRIG (2).

In NO_TRIG mode, all cyclic sources run continuously at the clock rate set by the setCyclicClock() method, and data source file reads do not occur until an output channel is accessed.

In EXT_TRIG mode, cyclic sources perform a single operation and file sources are read once for each trigger occurrence.

In INT_TRIG mode, cyclic sources perform a single cycle and data source files are read once each time an output channel is accessed.

Returns NO_ERR if successful or BAD_PARAM if inchan is out of range or mode is invalid.

Simulator control and I/O methods:

genTrigger(inchan)

Generates a trigger event. Depending on the trigger mode, a trigger event will result in one iteration of a cyclic data source, or one record read from a data input file. Returns NO_ERR if successful or BAD_PARAM if inchan is out of range.

Input/output methods:

readData(outchan, block=True, timeout=1.0)

Returns the data available for the specified output channel from the output buffer. If blocking is enabled, this method will block the return to the caller until the data becomes available or the specified timeout period has elapsed.

Returns a 2-tuple consisting of the return code and the data value from the output channel. Returns NO_ERR if successful or BAD_PARAM if outchan is out of range. If the return code is anything other than NO_ERR, the data value will be zero.

sendData(inchan, dataval)

Writes caller-supplied data into the specified channel. The data in the input buffer will be read on each cycle of the simulator. Returns NO_ERR if successful or BAD_PARAM if inchan is out of range.

For all its apparent complexity, the simulator really boils down to just two main methods: readData() and sendData(). Everything else just sets the stage for what will occur between the inputs and the outputs.

Some simple examples

The following example code demonstrates the data flow from an input to an output in the simulator. It doesn’t use the optional user function, nor does it apply scaling or noise to the data:

#! /bin/python
# TestDevSim1.py
#
# Echos data written into the simulator back to the output.
#
# Source code from the book "Real World Instrumentation with Python"
# By J. M. Hughes, published by O'Reilly.

from    DevSim  import  DevSim
import  SimLib.RetCodes as RC
import  DevSimDefs as DS

def testDevSim1():
    simIO = DevSim.DevSim()

    # set up the simulated device
    simIO.setInputSrc(DS.INCHAN1, DS.EXT_IN)
    simIO.setOutputSrc(DS.OUTCHAN1, DS.INCHAN1)

    loopcount = 0
    while loopcount < 10:
        simIO.sendData(DS.INCHAN1, (5.0 + loopcount))
        print simIO.readData(DS.OUTCHAN1)
        loopcount += 1

    simIO.stopSim = True    # set the stop flag

Referring to the code and to Figure 10-6, the first points of interest are the setInputSrc() and setOutputSrc() statements. In both method calls, the first parameter identifies the input or output channel to use and the second parameter specifies where the data will come from. The first call states that the data for input channel 1 will be obtained from the channel 1 external input, which occurs when the sendData() method is called. In other words, it controls the “Input Channel Source Select” (the small two-input trapezoid symbol in Figure 10-6). The second method call (setOutputSrc()) determines where the data that will appear on output channel 1 will be obtained. In this case, the selection parameter governs the behavior of the eight-input source selector, and it specifies input channel 1. Note that input channel 1 could mean either a direct input from an external source, or one of the internal cyclic data sources. If we follow the data path in Figure 10-6 we see that it goes through the user function block in the input side, and then through a scaling block and a summing junction for random data (noise) on the output side. This example does not use any of those features, so it’s effectively a straight connection between the input and the output.

The second example uses a data file as the input to the simulator:

#! /bin/python
# TestDevSim2.py
#
# Reads data from an input file and passes the data to the output. A
# data file containing 10 values in ASCII form must exist in the
# current directory.
#
# Source code from the book "Real World Instrumentation with Python"
# By J. M. Hughes, published by O'Reilly.

from    DevSim  import  DevSim
import  SimLib.RetCodes as RC
import  DevSimDefs as DS

def testDevSim2():
    simIO = DevSim.DevSim()

    # define the source file to use
    rc = simIO.setDataFile(DS.SRCFILE1, None, "indata1.dat")
    if rc != RC.NO_ERR:
        print "Error opening input data file"
    else:
        # set up the simulated device
        simIO.setOutputSrc(DS.OUTCHAN1, DS.SRCFILE1)

        loopcount = 0
        while loopcount < 10:
            # read and print data from file
            print simIO.readData(DS.OUTCHAN1)
            loopcount += 1

    simIO.stopSim = True

The call to the method setDataFile() defines the data file that will be associated with the index SRCFILE1. Note that setDataFile() also opens the file, and will return an error value of OPEN_ERR if the open fails. In this simple example we’re not checking the value of the error code, except to determine whether it’s anything other than NO_ERR. The call to setOutputSrc() associates OUTCHAN1 with the data file. Whenever the readData() method is called, the file is accessed and a single entry is read. The data is then passed through the eight-input source selector (see Figure 10-6) to the output scaling and random data summing stages, and finally appears on the output. Calling readData() is, in effect, a trigger event for the internal data file read operation.

Since there is no scaling and no random data “noise” applied to the values from the input file, this is basically just a way to open and read a data file. However, it’s also a useful building block for creating a more complex simulation setup, as shown in Figure 10-8. Here, DevSim is used to read data from an input file, process the data by applying a user-defined function and scaling, and then drive some test code with the resulting stimulus. This is just one way that DevSim can be leveraged to provide enhanced test and simulation functionality.

DevSim usage example
Figure 10-8. DevSim usage example

User-defined functions

DevSim incorporates the ability to insert a simple user-defined function into the input data stream just after the input source selector. A user-defined function is a string containing a valid Python statement. It is processed internally using Python’s built-in eval() method, and two predefined variables, x0 and x1, are provided. x0 is the current input data value, and x1 is the last current data value (a 1/z unit delay). You can actually do quite a bit with just these two variables. The result of the statement evaluation becomes the output to the eight-channel source selector. If the function string is empty, it is ignored.

For example, the statement x0 * 2 will multiply the input data by 2; it’s a simple scaling function. However, the statement (x0/(x0**2))+x1 is rather more interesting. It is an exponential expression, and given a linear series of input values (say, 0 to 10) it will generate an output that will produce a graph like the one shown in Figure 10-9.

The main restrictions on what you can do with a user-defined function are as follows:

  1. A user-defined function cannot refer to any variables other than x0 and x1.

  2. The x0 and x1 variables are read-only (i.e., inputs).

  3. A user-defined function cannot contain an assignment.

  4. A user-defined function must be a single statement; multiple statements and conditionals are not allowed.

Even with these restrictions there is quite a lot that can be accomplished with user-defined functions, including the simulation of physical characteristics such as inertial lag or a simple digital filter.

The following example shows how a user-defined function is used:

#! /bin/python
# TestDevSim3.py
#
# Demonstrates the use of a user-defined function string.
#
# Source code from the book "Real World Instrumentation with Python"
# By J. M. Hughes, published by O'Reilly.

from    DevSim  import  DevSim
import  SimLib.RetCodes as RC
import  DevSimDefs as DS

def testDevSim3():
    simIO = SimDev.SimDev()

    # set up the simulated device
    simIO.setInputSrc(SimDev.INCHAN1, SimDev.EXT_IN)
    simIO.setOutputSrc(SimDev.OUTCHAN1, SimDev.INCHAN1)
    simIO.setFunction(SimDev.INCHAN1, "x0 * 2")

    loopcount = 0
    while loopcount < 10:
        # send data to simulator
        simIO.sendData(SimDev.INCHAN1, loopcount)

        # acquire data input from simulator with function applied
        rc, simdata = simIO.readData(SimDev.OUTCHAN1)
        if rc != RC.NO_ERR:
            print "SimDev returned: %d on read" % rc
            break

        print "%d %f" % (loopcount, simdata)

        loopcount += 1
    # loop back for more

    simIO.stopSim = True

This example is just the 2x scaling function mentioned earlier.

Plot of (x0/(x0**2))+x1
Figure 10-9. Plot of (x0/(x0**2))+x1

Cyclic functions

The cyclic functions of the simulator provide a convenient source of predictable cyclic data. You have complete control over the wave shape and the timing, so you can, for example, set up a simulation using the pulse mode to evaluate the response of a control algorithm. The other cyclic functions might be used to simulate the velocity feedback from a mechanism, to simulate temperature change over time, or as a source of data to check limit sensing.

Because the cyclic functions have the ability to run in an asynchronous mode, you need to make sure that you don’t end up with a situation where the cyclic data is aliased because the data read rate is too slow, as shown in Figure 10-10. In this case, either the sample rate needs to be increased or the cyclic rate needs to be decreased so that the sample data read rate is no less than four times the cyclic rate.

Aliased cyclic data readout
Figure 10-10. Aliased cyclic data readout

Noise

Lastly, there is the random data injection that occurs just before the output. The relative range, or level, of both the data and the random “noise” are controllable. This allows you to set the balance between the two. Note that the random data is summed into the data stream, not simply injected. It won’t allow for the simulation of discrete transient events, but it does simulate modulation noise, such as might be found in a noisy voltage source (or a corroded connector).

AC Power Controller Simulator

The next example we’ll look at is a simulation of an eight-channel AC power control unit of the type often found in large server installations, laboratories, and industrial facilities. This example is intended to show how a simple command-response-type instrument behaves, and also to provide some insight into instruments that employ a serial interface. The concepts introduced here can also be seen in devices such as laser controllers, electronic test equipment, temperature controllers, and motion control units. We’ll also look at how to communicate with a serial interface simulator without using a second computer and a physical cable.

The SPC model

This simulation models a hypothetical device called the Simple Power Controller (SPC). It doesn’t have some of the bells and whistles found on real units, such as password protection, controller unit ID assignment, and so on, mainly because these features aren’t really necessary to control power. It does, however, simulate the inclusion of electronic circuit breakers (ECBs) for each AC channel. These are similar to the power control devices found in aerospace applications. They protect each channel from an over-current condition and can be reset remotely if necessary.

A diagram of the hypothetical SPC controller is shown in Figure 10-11. As you can see, it’s rather simple electrically (most devices like this are, actually). The “smarts” of the unit are contained entirely within its microcontroller.

In Figure 10-11, the blocks labeled SSR are solid state relays, and the microcontroller could be any suitable device. Notice that the diagram does not show any front-panel controls, because the simulator won’t be concerned with them. The objective of the simulator is to emulate the functionality of the microcontroller in terms of the remote control interface. You are, of course, welcome to add a nice GUI if you wish (we’ll discuss user interfaces in Chapter 13).

The SPC serial interface and virtual serial ports

Unlike the DevSim simulator with its API, the SPC simulator uses a serial interface. It’s a simple 9,800-baud N-8-1–type interface that is implemented using the pySerial library, which we will examine in detail in Chapter 11. For now we can assume that pySerial provides all the necessary functionality to open a serial port, set the serial port parameters, and read and write data.

There’s a catch to using a simulator that employs a serial interface, and that is interfacing with it. One way is to use two computers with a null-modem cable between them (as shown in Chapter 7), but a second computer is not always a viable option, and some machines don’t even have a serial port available (such as notebook and netbook PCs).

The solution is to use what are called “virtual ports” to create a link between two applications that utilize a serial interface. One such utility for the Windows environment is Vyacheslav Frolov’s com0com package. You can download it from http://com0com.sourceforge.net.

AC power controller simulatorsimulator block diagramAC simulator block diagram
Figure 10-11. AC simulator block diagram

com0com works by creating a pair of virtual serial ports configured as a null-modem connection. You can assign standard names to the ports when they are created in order to accommodate applications that can only deal with names like COM1, COM2, COM3, and so on, and from an application’s perspective these ports will behave exactly like real physical interface ports. You can set the baud rate, query the status, and in general do just about anything that can be done with a normal serial port. Figure 10-12 shows how instrumentation software can communicate with a serial interface–type simulator using two virtual serial ports.

Linux users can instead use tty0tty, which is available from http://tty0tty.sourceforge.net. It’s rather minimal, and the documentation is somewhat thin, but the user-level version worked just fine for me on Ubuntu 10.04 after compilation.

Using com0com
Figure 10-12. Using com0com

Communicating with SPC

There are basically two ways to interact with the SPC simulation: directly via a terminal emulator, or by way of instrumentation software. If you just want to manually exercise the simulator, you will need some way to send commands and view the responses. The tool of choice here is a terminal emulator. It doesn’t really matter what terminal emulator you use, so long as you can configure it to use the virtual serial ports that com0com (or tty0tty) creates.

On Windows 2000 and XP systems you can use the venerable Hyperterm or you may wish to check out Tera Term (which we’ll look at shortly), and on Linux the minicom emulator is available. If you are using Windows Vista or Windows 7, you will need to look around for a terminal emulator, but there are many available. When using a terminal emulator and virtual serial ports the setup will be basically the same as that shown in Figure 10-12, but with “Instrument Software” replaced by “Terminal Emulator.”

The SPC command set

SPC utilizes 10 simple commands to control outputs, check status, and set configuration parameters. All commands are three characters in length, with no exceptions, and all commands take at least one parameter. The commands are listed in Table 10-2.

Table 10-2. SPC commands

Command

Parameters

Description

ALL

state

Enables or disables all eight AC channels at once

POW

ch, state

Sets the power state of a specific channel to either On or Off

SEQ

state

Starts either a power-up or a power-down sequence

STM

time

Sets the delay time between sequence steps, in milliseconds

SOR

ch, ch, ch, ch, ch, ch, ch, ch

Defines the power-up and power-down sequence

SEM

mode

Sets the sequence error-handling mode

CHK

ch

Returns the on/off status of a specific channel or all channels

ECB

ch

Returns the OK/error status of a specific ECB or all ECBs

LIM

ch, amps

Sets the ECB current limit for a specific channel

RST

ch

Resets the ECB for a specific channel

Command descriptions

Here are a few general notes on the SPC commands:

  • All commands require at least one parameter. There are no zero-parameter commands.

  • Channels are identified by ASCII digits between 1 and 8, inclusive. The value 0 is used to indicate all channels with the CHK command.

  • The ASCII digits 1 and 0 are used to indicate On and Off, respectively, for the ALL and POW commands, and startup and shutdown for the SEQ command.

  • The sequence mode command, SEM, takes a single ASCII digit (0, 1, or 2) to indicate the mode selection.

  • Time and current limit values used with the STM and LIM commands are whole integer values in ASCII form, and are limited to two digits (0 to 99) for current limits (in amps) and three digits (0 to 999) for time (in seconds).

  • The SOR command takes a comma-separated list of one to eight channel numbers.

  • All commands return a response; there are no silent commands. Command responses are either 1 or 0. A 1 indicates either On or OK, and a 0 indicates either Off or an error.

Now, let’s take a closer look at the commands:

ALL state

Enables or disables all eight AC channels in sequence order without a dwell delay.

The state parameter may be 1 (On) or 0 (Off).

Responds with 1 (OK) if successful, or 0 (error) if the ECB is tripped for any channel at power-up. Returns immediately (does not wait for command completion).

POW ch, state

Sets the power state of a channel to either On or Off.

ch is a channel number, and state may be 1 (On) or 0 (Off).

Responds with 1 if successful, or 0 if the ECB is tripped at power-up or some other error occurred. Waits for command completion before returning.

SEQ state

Commands the controller to start either a power-up or power-down sequence. If no sequence order has been defined using the SOR command, the startup order will be from lowest to highest and the shutdown order will be the inverse.

The parameter state may be 1 (startup) or 0 (shutdown).

Responds with 1 if successful, or 0 if the ECB is tripped for any channel at power-up. Returns immediately (does not wait for command completion).

STM time

Sets the amount of time to pause between each step in a power-up or power-down sequence. The default pause time is 1 second, and the time is specified as an integer value.

Responds with 1 if successful, or 0 if the time value is invalid.

SOR ch, ch, ch, ch, ch, ch, ch, ch

Defines the startup and shutdown sequence order. Shutdown is the inverse of startup. The list may contain from one to eight channel ID entries. Any channel not in the list will be excluded from sequencing, and unused list positions are marked with a 0.

Responds with 1 if successful, or 0 if a sequence parameter is invalid.

SEM mode

Sets the error handling for power-up sequencing, where mode is defined as follows:

0

Normal (default) operation. If the ECB for any channel trips, the controller will disable power to any channels that are already active, in reverse order.

1

Error hold mode. If the ECB for any channel trips, the controller will halt the startup sequence but will not disable any channels that are already active.

2

Error continue mode. If the ECB for any channel trips during a startup sequence, the controller will continue the sequence with the next channel in the sequence list.

Responds with 1 if successful, or 0 if the mode is invalid.

CHK ch|0

Returns the on/off/error status of channel ch as either 1 or 0. If ch is set to 0, the statuses of all eight channels are returned as a comma-separated list of channel states. Also returns a 0 character for a channel if that channel’s ECB is tripped. Use the ERR command to check the ECB state.

ECB ch|0

Returns the ECB status of channel ch as either 1 (OK) or 0 (error). If n is set to 0, the ECB statuses of all eight channels are returned as a comma-separated list of states.

LIM ch|0, amps

Sets the current limit of the ECB for channel ch. If 0 is given for the channel ID, all channels will be assigned the limit value specified by amps.

Responds with 1 if successful, or 0 if the channel ID or current limit value is invalid.

RST ch

Attempts to reset the ECB for channel ch.

Responds with 1 if successful, or 0 if the ECB could not be reset.

SPC simulator internals

The SPC is a simple command-response-type device. It will never initiate communications with the host system. This means that it can be effectively simulated using a simple command recognizer. Also, since the SPC is a discrete state-based simulator, it needs a set of data to define the state of each of the power control channels. How and when a channel transitions from one mode to another is determined by the commands described in the previous section. Figure 10-13 shows the internal data that the SPC simulator needs in order to model a physical system.

SPC internal data
Figure 10-13. SPC internal data

The sequence control data objects apply to all channels, and each channel has three attributes: power state (1 or 0), ECB state (1 or 0), and a current limit for the channel’s ECB.

Next up is the command recognizer, which is very simple:

    def Dispatch(self, instr):
        cmdstrs = instr.split()

        if len(cmdstrs) >= 2:
            if len(cmdstrs[0]) == 3:
                if cmdstrs[0].upper() == "ALL":
                    self.SetAll(cmdstrs)
                elif cmdstrs[0].upper() == "POW":
                    self.SetPower(cmdstrs)
                elif cmdstrs[0].upper() == "SEQ":
                    self.SetSeq(cmdstrs)
                elif cmdstrs[0].upper() == "STM":
                    self.SetSTM(cmdstrs)
                elif cmdstrs[0].upper() == "SOR":
                    self.SetOrder(cmdstrs)
                elif cmdstrs[0].upper() == "SEM":
                    self.SetSEM(cmdstrs)
                elif cmdstrs[0].upper() == "CHK":
                    self.ChkChan(cmdstrs)
                elif cmdstrs[0].upper() == "ECB":
                    self.ChkECB(cmdstrs)
                elif cmdstrs[0].upper() == "LIM":
                    self.SetLimit(cmdstrs)
                elif cmdstrs[0].upper() == "RST":
                    self.RstChan(cmdstrs)
                else:
                    SendResp("ER")
            else:
                SendResp("ER")
        else:
            SendResp("ER")

After a command is received, the method Dispatch() is called. The incoming string from the host system is split into a list, which should contain two or more elements. The first element should be the command keyword. The number of parameters after the keyword will vary depending on the command, but no command has zero parameters.

When the command is decoded, one of 10 utility methods is called to write data into the internal data table, get data from the table, and invoke the channel control to perform the commanded action (if it’s not just a status query). Notice that there is a command utility method for each command.

Figure 10-14 shows the message sequence chart (MSC) for a typical command-response interaction with the SPC simulator.

SPC command-response MSC
Figure 10-14. SPC command-response MSC

There are two possible return paths from the SPC command processor back to the host (the terminal emulator or instrumentation software), depending on the command. Some commands provide an immediate response and do not wait for the channel control logic to complete an activity. Other commands will wait and then return an indication of success or failure.

Configuring the SPC

The SPC simulator uses a configuration file, also known as an “INI” file, to hold various configuration parameters that are read in at startup. Here is what spc.ini might look like:

[SPC]
SPORT=COM4
SBAUD=9600
SDATA=8
SPAR=N
SSTOP=1
ECB1=2.5
ECB2=2.5
ECB3=5.0
SOR=[3,2,1,4,5,8,7,8,6]
STM=2.0
SEM=0

The values from the INI file will be loaded into the internal data table, but they may be overwritten with new values using the SPC commands. Parameters not defined in the INI file will assume their default values.

Interacting with the SPC simulator

When the SPC simulator is started, it will first attempt to open the serial port defined in the INI file. If successful, it will start its primary loop and wait for incoming commands. When the SPC receives a carriage return (CR) and nothing else, it will return the prompt character (>). Here’s an example session that enables power channel 1:

> CHK 0
[0, 0, 0, 0, 0, 0, 0, 0]
> POW 1, 1
1
> CHK 0
[1, 0, 0, 0, 0, 0, 0, 0]

To use the SPC simulator with instrument software, the first step is to get the SPC’s attention. Something like the following code snippet should suffice (we’ll assume that a serial port is already open and referenced by the sport object):

gotprompt = False
last_time = time.time()
MAXWAIT = 5.0

while True:
    sport.write("
")
    instr = sport.read(2)
    if instr=="> ":
        gotprompt = True
        break
    if time.time() - last_time > MAXWAIT:
        break
    time.sleep(0.5)

This snippet will send a carriage return character every 500 milliseconds until the SPC responds or five seconds has elapsed, whichever comes first. When the SPC responds with a prompt, it exits the loop, and the system is ready to communicate.

Using the SPC as a framework, you can create a simulator for just about any simple device or instrument with a serial control interface. The SPC simulator also shows how a simulation can be used to evaluate a device or instrument that does not (yet) exist in the real world. There is no substitute for working with a live system, be it real or simulated, to get a feeling for what it can, and cannot, do.

Serial Terminal Emulators

When working with instruments and subsystems that employ a serial interface, it is sometimes possible to repurpose some commonly available tools to create a perfectly usable simulator.

One such tool for Windows systems is called Tera Term; this is the tool I will focus on in this section. Originally written by T. Teranashi in the mid-1990s (and last updated in 1999, when version 2.3 was released), Tera Term supports Telnet logins as well as serial I/O, but the original release of 2.3 does not support SSH.

Although there is no longer a big demand for serial terminal emulators, and Tera Term is getting rather dated, it has something that makes it particularly interesting: a powerful scripting language. When combined with a tool such as com0com, it is possible to use Tera Term to create a respectable simulation of a serial I/O instrument and communicate with it from your instrumentation software during development and testing. Tera Term works well at the other end of the communications link as well, and I’ve used it as a functional test driver for an embedded imaging system and a laser interferometer system, among other applications. Of course, I have also used it many times as just a terminal emulator.

You can download Tera Term and get more information about it from http://hp.vector.co.jp/authors/VA002416/teraterm.html. The source code is freely available, and there are some add-ons available as well. Check the website for details.

Installing Tera Term is easy. After downloading the archive, unzip it into a temporary location. Then find and run the file setup.exe. This will install Tera Term in c:Program FilesTTERMPRO (unless you specify a different location). After the installation is complete, you can delete the contents of the temporary directory.

The installation will create a subsection in the Windows StartPrograms menu called “Tera Term Pro.” You can use the right mouse button to drag the program icon labeled “Tera Term Pro” out onto the desktop and create an icon. Tera Term is known to work with Windows 2000 and Windows XP.

Almost all Linux installations come with a serial terminal emulator tool called minicom, and there are other emulators available as well. Some like, minicom, are rather limited in terms of their scripting capability, whereas others are more feature-complete. I tend to view Tera Term as a model of what a free and open source serial terminal emulator should be able to do, so I will stick to that for the rest of this discussion. If you are using a Linux system, by all means explore the options available to you. Given the huge amount of software available for Linux, I’m sure there is something that will meet your needs. In any case, after seeing what Tera Term can do you should have some idea of what to look for.

Using Terminal Emulator Scripts

The main focus of terminal emulator scripts is the conditional test. In other words: if something is this, then do that, else do another thing. Although a terminal emulator may have a lot of bells and whistles, a basic script to dial a number and announce a connection boils down to something like this:

:dial
send string "555-1212"
if busy then goto dial
wait connected
if connected print "CONNECTED"
if not connected print "ERROR - COULD NOT CONNECT"
exit

If we accept that the wait statement is actually a form of IF-ELSE statement, we can see that this simple script is really just a sequence of conditional tests.

I’m making a point of this because once you understand the paradigm behind the scripting languages employed in terminal emulators it becomes much easier to leverage these tools into roles for which they were never originally intended.

The Tera Term scripting language (Tera Term Language, or TTL as the author calls it) is a full-featured language that not only provides the basic commands for handling IF-THEN decisions in a communications context, but also includes commands for generating dialog windows, writing data to files, reading from files, string conversions, and executing external applications. The language provides flow control statements such as IF-THEN (plus ELSEIF and ELSE), FOR, and WHILE. It does not have a complete set of math functions, and it supports only two data types: integers and strings. But, given what it was originally intended to do, this makes perfect sense, and it really isn’t a major hurdle. You can view the online documentation included with Tera Term by selecting the Help menu item (there is no user manual). Note that the main help display refers to the scripting facility as “MACRO.”

Here is the connection test for the SPC that we saw earlier in Python, translated into TTL:

waitcnt = 0

:connect
; check for max attempts
if wantcnt > 9 then
    goto noconnect
endif

send ""
recvln
; recvln puts its return into "inputstr"
strcompare inputstr  "> "
; strcompare sets "result" based on the comparison
if result = 0 then
    goto start
else
    ; pause only uses whole numbers
    pause 1
    ; jump back and try it again
    goto connect
endif

:start
; skip over the error dialog display
goto endconnect

:noconnect
messagebox "SPC not responding" "Error"

:endconnect
; at this point the user can start entering commands

If you’ve ever worked with BASIC, or the so-called batch files on an MS-DOS or Windows system, this should look familiar. Tera Term’s TTL does support subroutines (CALL-RETURN), and it has a perfectly usable WHILE statement, but I elected not to use them in this example.

Like most terminal emulators, Tera Term handles only one external connection at a time, so it’s not really possible to employ Tera Term as the control logic in an instrumentation system (at least not easily—this can be done using external programs and data files). Where Tera Term is useful is in creating a simulation of an instrument or device for a Python instrumentation application to communicate with. One could, in fact, implement the SPC simulator entirely in TTL using Tera Term. Almost any other simple instrument with a serial interface could also be a candidate for simulation using Tera Term.

Tera Term is also useful for driving other systems for repetitive testing. In fact, during the development of the image acquisition and processing software for a space probe, Tera Term was used to push tens of thousands of test images through the image compression software and log the results of each test. It worked flawlessly, and generated a mountain of data to sift through.

Displaying Simulation Data

A simulation can generate a lot of useful data, but just looking at a file with a list of numbers isn’t as intuitive as seeing a graph of the data. In this section I will show you how to use the data generated by a simulator to create interesting and useful graphical output in the form of data plots.

We’ll focus on gnuplot, a venerable tool that has been available for Unix and Linux systems for many years. There is also a Windows version available, and both can work with Python to display dynamically generated data. Later, in Chapter 13, we’ll look at user interfaces and more sophisticated ways to generate graphical output, but this is a good place to begin.

gnuplot

gnuplot is a powerful and well-established graphical plotting tool that is capable of generating graphical output ranging from simple line graphs to complex data visualizations. Although it was originally developed for Unix, there is a Windows version available as well. gnuplot has a serviceable built-in command-line-style user interface and the ability to load plot command and data files. It can also use so-called pipes for its command input, thus allowing other applications to drive the plot display. This section briefly describes two methods to allow Python programs to send data and commands to gnuplot for display. The first is a simple demonstration of Python’s popen() method. While this method is straightforward and easy to implement, it does nothing to assist you with gnuplot; it just sends commands. Consequently, the programmer needs to have a good understanding of the gnuplot application and its various command and configuration options. The second method uses Michael Haggerty’s gnuplot.py package, which implements a wrapper object for gnuplot that handles some of the details of the command interface for the programmer. The documentation for gnuplot is contained in a set of HTML pages included with the distribution, and is also available online at http://gnuplot-py.sourceforge.net/doc/Gnuplot/index.html.

The first step is to install gnuplot, if it isn’t already installed on your system. If you’re running Linux, there’s a good chance it’s already available. If it isn’t, a quick session with a package manager (apt-get, rpm, synaptic, etc.) can be used to do the installation. If you’re running Windows, you’ll need to download the gnuplot installation package from SourceForge. If you are so inclined, you can also download the source code and build it from scratch (not recommended unless you really know what you’re doing and your system doesn’t have a package manager tool available). The current version of gnuplot (at the time of publication) is 4.4.2. It is available from http://gnuplot.sourceforge.net.

Installing gnuplot on Windows

I will assume that Linux users will install gnuplot using a package manager, so I won’t describe the process here. This procedure applies only to Windows users.

If installing the support packages for the first time, perform the setup in the following order:

  1. Unzip the archive file gp440win32.zip to your C: drive, or somewhere else where you would like it to live permanently. You might even put it under “Program Files” on the C: drive. A directory called gnuplot will be created when the archive is unzipped.

  2. Open the environment variables dialog (on Windows 2000 and XP) by using SettingsControl PanelSystemAdvancedEnvironment Variables, or right-clicking on the “My Computer” icon on the desktop and selecting “Properties.” Set the GNUPLOT environment variable (as a system variable, not a user variable) to refer to the directory binary in the gnuplot directory structure. See the INSTALL file in the gnuplot directory for more information about the various environment variables available.

    Using the Environment Variables dialog, put the gnuplotinary directory into the Windows search path (the PATH environment variable). For example, if you put gnuplot in the root of the C: drive, you would add C:gnuplotinary to the search path string. Note that entries in the path string are separated by semicolons.

  3. Optionally, you can also create an icon on the desktop to launch gnuplot using the file wgnuplot.exe. Make sure that the “Start in” parameter refers to the gnuplotinary directory (right-click on the icon and select Properties from the menu that appears).

  4. Look through the documentation found in the directory gnuplotdocs, and the file gnuplot-4.4.0.pdf in particular. Also read README.windows, located in the root directory of the gnuplot directory tree.

The gnuplot package for Windows contains the following executable files:

wgnuplot.exe

A Windows GUI version of gnuplot. Provides the same command-line console interface as the non-GUI version, but uses a GUI text-editor-type display for the command line and includes a menu bar and buttons to click for common operations.

wgnuplot_pipes.exe

Same as wgnuplot, but with the advantage of support for internal pipe specifications of the form:

plot `<awk -f change.awk data.dat`
gnuplot.exe

The classic text (console) interface version of the gnuplot executable, with all the associated pipe functionality as found on other platforms. This means that this program can also accept commands on stdin (standard input) and print messages on stdout (standard output). This is the preferred executable to use when integrating gnuplot with other programs, such as Python applications.

pgnuplot.exe

A “helper” program that will accept commands on stdin (standard input) and pipe them to an active (or newly created) wgnuplot.exe console window. Command-line options are passed on to wgnuplot.

wgnuplot is what you would typically put on the Windows desktop as an icon, and gnuplot is what a Python program would open when creating a pipe.

Using gnuplot

As I stated earlier, we are going to look at two ways to interface with gnuplot from a Python application. The first is simple, but requires a solid grasp of the gnuplot command set. The second method handles a lot of the details for you, but it also hides some of those command details from you, and it implements someone else’s notion of what a Python-gnuplot interface should be. It’s up to you to pick the path of least resistance to accomplish your objectives, and you should at least skim through the available documentation for gnuplot and gnuplot.py before deciding which method makes the most sense for your application.

Method 1: Using Python’s popen() method

If you want to be able to use a pipe to send commands to gnuplot running under Windows, you must use the gnuplot version, not wgnuplot. This is because Windows GUI applications (such as wgnuplot) do not accept input from stdin. You can tell Python to use gnuplot when creating the pipe using popen(). Alternatively, you can use pgnuplot to achieve the same results with wgnuplot. On a Linux system this is not an issue (there are no wgnuplot or pgnuplot binaries).

The following example, gptest.py, will launch gnuplot and display a series of plots:

#! /usr/bin/python
# gptest.py

import os
import time

f=os.popen('gnuplot', 'w')

print >> f, 'set title "Simple plot demo" 1, 1 font "arial, 11"'
print >> f, 'set key font "arial, 9"'
print >> f, 'set tics font "arial, 8"'

print >> f, "set yrange[-20:+20]"
print >> f, "set xrange[-10:+10]"
print >> f, 'set xlabel "Input" font "arial,11"'
print >> f, 'set ylabel "Output" font "arial,11"'

for n in range(100):
    # plot sine output with zero line (the 0 term)
    print >> f, 'plot sin(x * %i) * 10, 0' % (n)
    time.sleep(0.1)

f.flush()

# pause before exit
time.sleep(2)

To run this example, just save the code to a file (gptest.py, for example) or load the file from the source code for this book. On a Windows machine, type in the following at a command prompt:

python gptest.py

Under Linux you can just enter the script’s name, assuming that the file is marked as executable and the Python interpreter resides in /usr/bin:

% gptest.py

When it runs, you should see a sine wave that expands and contracts several times. What is actually happening is that gnuplot is regenerating the plot across the x range of −10 to +10 each time the plot command is called. The result appears as an animated image, but in fact it is a series of plots presented in rapid succession. Note that the gnuplot window will close as soon as the script completes and Python terminates.

The following lines from the Method 1 example are possible candidates for inclusion into the gnuplot.ini file, which should be located in the binary directory with the gnuplot executables:

set key font "arial, 9"
set tics font "arial, 8"

Refer to the gnuplot documentation for more about the gnuplot.ini file and its uses.

Method 2: gnuplot.py

The second method uses Michael Haggerty’s gnuplot.py package (version 1.8), which is available from http://gnuplot-py.sourceforge.net. You will also need the NumPy package (version 1.4.1), available from http://numpy.scipy.org. For Windows, you should download and install numpy-1.4.1-win32-superpack-python26.exe (just execute the file to start the installation).

To install gnuplot.py, follow these steps:

  1. Unzip gnuplot-py-1.8.zip into a temporary directory. It will create a directory called gnuplot-py-1.8.

  2. Open a command window, and from within the command window change to the directory gnuplot-py-1.8 in the temporary directory where gnuplot-py-1.8 was unpacked. You should see a file called setup.py.

  3. At the command prompt, type:

    python setup.py install

During the execution of the setup script, you should see many lines of output go by on the screen. If an error is encountered, the setup script will halt; otherwise, you should now be ready to go.

Testing gnuplot.py

In the directory <python>Libsite-packagesGnuplot run the file demo.py, like so:

python demo.py

<python> is where you installed Python 2.6, and on a Windows system it may be something like C:Python2.6.

If all goes well, you’ll see a plot display. Pressing the Enter key from within the command window will cause a series of graphs to be displayed.

Using gnuplot.py

gnuplot.py is somewhat unusual in terms of how it is imported into your application. If you look at demo.py, you’ll see that it is importing Gnuplot and Gnuplot.funcutils. But there is no “Gnuplot.py” in the Gnuplot directory where the package resides. What is happening here is that the package initializer, __init__.py, is imported. It, in turn, imports the rest of the necessary modules. The __init__.py module also contains a top-level docstring with lots of information. If you examine __init__.py you may also notice that it contains the following code at the bottom of the file:

if __name__ == '__main__':
    import demo
    demo.demo()

What this means is that you can type in:

python __init__.py

from the Gnuplot directory, and the demo will execute.

This approach is interesting in that when the gnuplot.py package is imported the __init__.py module will be evaluated immediately, and the necessary imports will be in place and available from that point onward in your application.

To import gnuplot.py into your application, you must at least import the main module:

import Gnuplot

You can import additional modules using dot notation, like so:

import Gnuplot.funcutils

Plotting Simulator Data with gnuplot

This next couple of examples will plot the contents of a data file containing a set of records with a single field (we will look at ASCII data files in more detail in Chapter 12).

We’ll use the PID class introduced in Chapter 9, and create a data file to graph the impulse response of the controller.

First, here’s how to do it using the pipe method:

#! /bin/python
# PIDPlot.py
#
# Uses gnuplot to generate a graph of the PID function's output
#
# Source code from the book "Real World Instrumentation with Python"
# By J. M. Hughes, published by O'Reilly.

import time
import os
import PID

def PIDPlot(Kp=1, Ki=0, Kd=0):
    pid = PID.PID()

    pid.SetKp(Kp)
    pid.SetKi(Ki)
    pid.SetKd(Kd)

    time.sleep(.1)
    f = open('pidplot.dat','w')

    sp = 0
    fb = 0
    outv = 0

    print "Kp: %2.3f Ki: %2.3f Kd: %2.3f" %
           (pid.Kp, pid.Ki, pid.Kd)

    for i in range(1,51):
        # summing node
        err = sp - fb

        # PID block
        outv = pid.GenOut(err)

        # control feedback
        if sp > 0:
            fb += (outv - (1/i))

        # start with sp = 0, simulate a step input at t(10)
        if i > 9:
            sp = 1

        print >> f,"%d  % 2.3f  % 2.3f  % 2.3f  % 2.3f" %
                   (i, sp, fb, err, outv)
        time.sleep(.05)

    f.close()

gp=os.popen('gnuplot', 'w')
print >> gp, "set yrange[-1:2]"

for i in range(0, 10):
    kpval = 0.9 + (i * .1)
    PIDPlot(kpval)
    print >> gp, "plot 'pidplot.dat' using 1:2 with lines, 
                       'pidplot.dat' using 1:3 with lines"

raw_input('Press return to exit...
')

This will generate a series of graphs, with a Kp value ranging from 0.9 to 1.8. The last graph is shown until the user presses the Enter key, and when the program terminates gnuplot closes. The output for the last graph (Kp = 1.8) is shown in Figure 10-15.

As you may recall from Chapter 9, if the proportional gain is too high the system will exhibit instability in response to a sudden input change, and there it is in Figure 10-15.

Now here’s the gnuplot.py way to plot the data:

# PIDPlot.py
#
# Uses gnuplot to generate a graph of the PID function's output
#
# Source code from the book "Real World Instrumentation with Python"
# By J. M. Hughes, published by O'Reilly.
import Gnuplot, Gnuplot.funcutils

import time
import os
import PID

def PIDPlot(Kp=1, Ki=0, Kd=0):
    pid = PID.PID()

    pid.SetKp(Kp)
    pid.SetKi(Ki)
    pid.SetKd(Kd)

    time.sleep(.1)
    f = open('pidplot.dat','w')

    sp = 0
    fb = 0
    outv = 0

    print "Kp: %2.3f Ki: %2.3f Kd: %2.3f" %
           (pid.Kp, pid.Ki, pid.Kd)

    for i in range(1,51):
        # summing node
        err = sp - fb

        # PID block
        outv = pid.GenOut(err)

        # control feedback
        if sp > 0:
            fb += (outv - (1/i))

        # start with sp = 0, simulate a step input at t(10)
        if i > 9:
            sp = 1

        print >> f,"%d  % 2.3f  % 2.3f  % 2.3f  % 2.3f" %
                   (i, sp, fb, err, outv)
        time.sleep(.05)

    f.close()

gp = Gnuplot.Gnuplot()
gp.clear()
gp.title('PID Response')
gp.set_range('yrange', (-1, 2))

for i in range(0, 10):
    kpval = 0.9 + (i * .1)
    PIDPlot(kpval)
    gp.plot(Gnuplot.File('pidplot.dat', using=(1,2), with_='lines'),
            Gnuplot.File('pidplot.dat', using=(1,3), with_='lines'))

raw_input('Press return to exit...
')

The output looks the same as before, except that now there is a title above the graph. For some examples of the other things that gnuplot.py can do, see the demo.py and test.py files included with gnuplot.py and located in the gnuplot directory where the package was installed (usually python26/Lib/site-packages).

gnuplot PID graph
Figure 10-15. gnuplot PID graph

Creating Your Own Simulators

Now that we’ve touched on what simulators are and seen some ways they can be used, it’s time to consider what goes into creating such a thing. I’m a big fan of simulators, but I also try to temper my enthusiasm with some realism. It’s all too easy to eat up a big chunk of the time and budget for a project just fiddling with the simulation. So, before starting to build your own simulator, there are three key questions you should ask yourself:

  1. Why do you want to use a simulator?

  2. What do you want to simulate?

  3. How much time and effort can you expend to create a simulator?

How you answer these questions will help you avoid spending time on something you don’t really need (even if it is fun to build and play with).

Justifying a Simulator

First and foremost, there must be a real need for a simulation. If it is really not possible to develop the instrumentation or control software without one, that is probably enough justification. As I stated at the start of this chapter, such a situation might arise when software needs to be written, but the hardware won’t arrive until some later date. Rather than wait for the hardware, and run the risk of running over schedule as a result, you can use a simulation to start at least building and testing the framework of the instrumentation software.

Another example would be where the hardware is something unique and special, and there is a definite risk that the software could make it do something that might damage it. Some motion control systems fall into this category (recall the story of the runaway PID servo controller in Chapter 9), as might systems that involve high temperatures or pressures.

The Simulation Scope

When considering something like a simulator, it is essential to define the scope of the simulation before any code is written: in other words, what it will simulate, and how accurate the simulation needs to be in order to be useful.

Figure 10-16 shows one way to visualize the levels of detail and complexity that can go into a simulation. Starting at the bottommost level, I/O, the fidelity of the simulation increases as additional layers are added. However, the costs in terms of time and effort also increase.

In many cases, just having the I/O and command processing levels may be sufficient. The simulators we’ve already seen in this chapter really aren’t much more than this, with a little bit of functionality to handle setting and recalling states and parameters. As the size of the blocks in Figure 10-16 implies, the more capabilities you add, the more complex and costly the simulator becomes.

Although each simulator you might contemplate must be evaluated based on the specific needs of the project, it is generally safe to say that if you can accurately simulate the interface and the command processing, it’s probably good enough to get you started. You can always add details in the behavior level later if it turns out that they are really necessary.

Simulation levels
Figure 10-16. Simulation levels

Time and Effort

Writing a simulator takes time and effort. Sometimes this can be significant, and it is not uncommon to have a situation where creating and verifying the simulator takes as much (or more) effort than that required to implement the system being developed. This might be justified if the system is something critical (like part of an experimental aircraft), if the simulator is generic enough to be reused on other projects, or if it can be used later in a production environment for product testing. If it’s just a one-time thing for a simple system, it shouldn’t be any more complicated than it really needs to be. It might even turn out that a simulator isn’t really necessary at all if a good debugger is available, and you’re willing to use simple files to capture data.

Summary

When used appropriately, simulation is a powerful tool that can help save effort and avoid costly mistakes and delays later on, when the software meets real hardware. It is also a potential time-sink and source of development delays, so deciding when and how to employ simulation is important.

Suggested Reading

If you’re interested in digging deeper into simulation, the following books are good places to start:

Simulation Modeling Handbook: A Practical Approach. Christopher Chung (ed.), CRC Press, 2003.

A collection of papers and essays on simulation topics with a focus on practical applications rather than on theoretical issues. Provides step-by-step procedures and covers problem analysis, model development, and data analysis.

Software Fault Injection: Inoculating Programs Against Errors. J. Voas and G. McGraw, John Wiley & Sons, 1998.

One of the first books to deal with the subject of fault injection in depth. Although oriented toward high-end safety-critical and fault-tolerant systems, the techniques and concepts in this book can be applied to any software development project.

Gnuplot in Action. Philipp K. Janert, Manning Publications, 2009.

A thorough review of gnuplot, along with a wealth of ideas for how to use it to create useful and interesting data visualization displays.

There are also some general resources available on the Web, although there seems to be very little in the way of introductory material available. Here are a few URLs that may be of interest to you:

http://www4.ncsu.edu/~hp/simulation.pdf

This link points to the PDF version of Computer Simulation Techniques: The Definitive Introduction!, by Harry Perros. While not quite as broad in scope as the title might suggest, this book introduces traditional Operational Research (OR)–type simulations and contains extensive discussions of randomness, sampling theory, and estimation techniques.

http://sip.clarku.edu/index.html

The companion website for the book Introduction to Computer Simulation Methods, by Harvey Gould, Jan Tobochnik, and Wolfgang Christian (Addison-Wesley). While the book itself is not available here, there are lots of useful notes, tutorials, and a couple of sample chapters in PDF format. The main focus of the book itself is on computational physics and the simulation of physical systems. I’ve recommended it here mainly for the examples it provides of ways to approach various simulation problems using mathematical techniques.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.76.30