Software and People

I have recently had the nostalgic pleasure of writing some software in C, running under DOS) to read from a serial port. After the first stages of tinkering with it I began to really miss the power of Test Driven Development. Now I'm sure I'm addicted!

I thought for a while of trying to write some sort of test harness to run my code on the target machine, bu this has several problems. The major problem is simply the time it would take. I can't afford to spend a week or so building/testing a test harness when the code really needs to be delivered by then. I have looked for JUnit equivalents in C/C++ before, and never found any that were at all portable, so that chance of getting someone else's test framework running on this slice of history seems very slim. Other problems include my general unfamiliarity with the (very neat and quick, but different) OpenWatcom development environment and compiler I'm using, and the non-OO nature of C (which would imply quite a different architecture to JUnit and chums.)

However, a few days ago I had a flash of inspiration. It happened because I suddenly realized that when I run thid DOS .COM executable on my Windows 2000 machine, it actually runs it in a resource constrained "dos box", not a real take-over-the-whole-machine DOS installation at all. So I'm able to run my software on this machine, and run other, "full fat" software at the same time. I'd already bought a "nullmodem" cross-connected serial cable and tried the software on a remote machine using HyperTerminal. The next step was to connect the nullmodem lead between the two com ports on the back of the development PC and try running the DOS box and HyperTerminal on the same machine at the same time. Flushed with this success, I then did a quick hack to the DOS software to recognize a command-line parameter to switch from the default "receive" mode to a more active "transmit mode. I set them both running and happily watched my sequence of characters wander along the serial lead at a stately 1200 baud from one DOS box to another. Now, I was really making progress.

Comfortable that I could drive my software from the same machine over a cable, I now set out to set things up using a powerful test framework that I am very familiar with - JUnit. Java (and thus JUnit) already has the features to test things lke the existence and content of files, so that part of the project will be easy. What Java does not have "out of the box" is the ability to write and read serial ports. I vaguely recalled hearing of a Java "comms" API, and few minutes searching at Sun found it. I downloaded the Windows version, unzipped it and got started.

Or at least, I tried to get started. Maybe I've had a sheltered life, but I found the javax.comm API one of the strangest Java installs I've had to do in recent years:

  • The installation documents assume you are using Java 1.1 (yikes!). Admittedly, there is an addendum about using with the "new" beta version of Java 1.2, but it's not immediately obvious that that is the place to look for the real installation instructions.

  • As well as adding comm.jar to the class path, you also need to put a .dll file in JAVA_HOME/jre/bin (not JAVA_HOME/bin, as suggested in the main install docs), and add a supplied properties file to JAVA_HOME/jre/lib. If you don't do the .dll and properties files right (for example, by following the obvious installation instructions) the code compiles and appears to work, but just thinks the machine has no ports.

  • The supplied example code seems to have been written by someone just learning Java. It's very poor, and doesn't even take advantage of the features of the comm API it's supposed to be demonstrating. For example, the API provides a neat callback mechanism to notify your code when events happen (such as when an output buffer is empty, indicating that the supplied data has been sent). The serial writing example enables this feature, but then never registers a listener and instead hard-codes a two second wait to "be sure data is xferred before closing". Yuck. The code also commits the cardinal sin of catching exceptions and just "eating" them in an empty catch block. This is inexcusible in a code example which people will use to check if the system has been installed correctly.

  • I could find no API documentation on the web site, just a confusing maze of links which all eventually end up back at the same, (largely useless) page. I did find some API docs in the download, but they were generated using the old JavaDoc, without frames and font/layout tidyups, so they are very clumsy to navigate.

Despite all this, I did eventually get things going. To aid my unit testing I put together a simple serial port writing helper class. It only does the things I need right now, and I can see that it might hit problems If I try to run too many tests in too short a time (because of the asynchronous nature of port open and close), but it does allow me to drive my DOS software from JUnit.

package tests;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.TooManyListenersException;

import javax.comm.CommPortIdentifier;
import javax.comm.PortInUseException;
import javax.comm.SerialPort;
import javax.comm.SerialPortEvent;
import javax.comm.SerialPortEventListener;
import javax.comm.UnsupportedCommOperationException;

class CloseWhenEmptyListener implements SerialPortEventListener
{
  private SerialPort port;

  public CloseWhenEmptyListener(SerialPort port)
  {
    this.port = port;
  }
  
  public void serialEvent(SerialPortEvent event)
  {
    if (event.getEventType() == SerialPortEvent.OUTPUT_BUFFER_EMPTY)
    {
      port.close();
    }
  }
}

public class CommPort
{
  private int baud;
  private CommPortIdentifier port;

  private static List findSerialPorts()
  {
    List ports = null;
    
    if (ports == null)
    {
      ports = new ArrayList();

      Enumeration portList = CommPortIdentifier.getPortIdentifiers();

      while (portList.hasMoreElements()) 
      {
        CommPortIdentifier port = (CommPortIdentifier) portList.nextElement();

        if (port.getPortType() == CommPortIdentifier.PORT_SERIAL) 
        {
          ports.add(port);
        }
      }
    }
    
    return ports;
  }
  
  public static CommPortIdentifier findPort(String name)
  {
    CommPortIdentifier ret = null;
     
    List ports = findSerialPorts();
    
    Iterator it = ports.iterator();
    while (it.hasNext())
    {
      CommPortIdentifier port = (CommPortIdentifier)it.next();
      if (name.equalsIgnoreCase(port.getName()))
      {
        ret = port;
        break;
      }
    }
    
    return ret;
  }
  
  public CommPort(String name, int baud)
  {
    this.port = findPort(name);
    this.baud = baud;
  }
  
  public void write(String text)
    throws IOException, PortInUseException,
    UnsupportedCommOperationException, TooManyListenersException
  {
    SerialPort serialPort = (SerialPort) port.open("CommPort", 2000);
    serialPort.setSerialPortParams(baud, 
               SerialPort.DATABITS_8, 
               SerialPort.STOPBITS_1, 
               SerialPort.PARITY_NONE);
    OutputStream outputStream = serialPort.getOutputStream();

    serialPort.addEventListener(new CloseWhenEmptyListener(serialPort));
    serialPort.notifyOnOutputEmpty(true);
        
    outputStream.write(text.getBytes());
  }
}