Port I/O on a PC
The first issue we needed to address during the design of the system's software was how we would control pumps from a standard PC. There are two obvious ports to consider when addressing this problem: The serial and parallel ports.
Serial ports are designed, as the name implies, to send serial data. This means that there is a certain data rate (e.g. 2400bps) associated with communication on that port, and the data to be transferred is written to or read from the port at that rate. For our application, we needed to be able to switch various pumps on, keep them on for anywhere between a fraction of a second and ten or more seconds, and then turn them back off again. This is a natural application for the parallel port, which supports writing 8 bits of data in parallel, and does not require that the data on the port change at a fixed interval.
Usually, the device attached to the parallel port will use the various control lines to indicate that it is ready for more data, or that an error (such as a printer being out of paper) has occurred. We could simply ignore those control pins for our application, and focus on the 8 data pins to switch our pumps. In the future, those input lines could be used for receiving information from the pump-controlling circuitry, such as checking whether or not there is a cup in place before pouring a drink.
Parallel Port Control - How?
The next step was to determine how to control the parallel port from application software. At this point, we had not yet determined which programming language and GUI toolkit we would be using for the eventual user interface of the system. We also wanted to keep our options open as for as operating systems were concerned.
One potential way for controlling I/O ports is Sun's Javacomm API, which enables control of parallel and serial ports from Java. However, we found that Sun only provides implementations of this API for Solaris and Windows. Since we wanted to retain the ability to run our system under the Linux OS, Javacomm could not be used.
After some more research, we determined that parallel port I/O is very easy in Linux (using the C programming language), requiring only a small amount of code as long as the program doing the I/O is running with super-user privileges. We also discovered that, in Windows, parallel port I/O was only easy in the Windows 9x line. In Windows NT-based OS's (such as Windows 2000 and XP), it could not be done from a user-mode program, but instead required a kernel-mode device driver. We managed, however, to find such a driver available for free download. This driver is called PortIO, by Scientific Software Tools Inc. Using PortIO, we could now do very easy parallel port I/O in C on any Windows OS.
Now we had our answer for parallel port output and cross-platform support. We would write our I/O code in C, ensuring that we had a decent level of abstraction over the parallel port I/O routines so that we could port this code to other platforms with some simple C pre-processor directives doing conditional compilation. Our initial implementation would be for Windows using the PortIO library and driver, since we had a Windows XP laptop to use for testing our circuitry.
The IOWrapper Library
In order to make it as simple as possible to develop different user interfaces for the system using different technologies, it was decided that we would make a C library that encapsulated all the low-level details of the system. This library, which we call the "IOWrapper", handles these major tasks: Writing to the parallel port, determining how long to run the pumps to pour a given quantity of liquid, and scheduling the running of multiple pumps at the same time. In addition, the IOWrapper handles various special cases such as the drop in flow rate per pump when two pumps are operated simultaneously, and the adjustments needed to ensure that pop syrup is never poured after soda water when making pop. Because C calling conventions are supported by a very large number of programming languages, we are able to invoke the IOWrapper from whichever GUI technology we choose.
The next required component of the system's software was some sort of database of drinks to pour. We purchased a CD-ROM that contains "over 1000 drinks", and discovered that the CD-ROM's drink database was stored in a Microsoft Access database file. After exporting that Access database to an XML file, we had our platform-agnostic drinks database. A Java object model (see Fig. 1) was created to store the various drinks, ingredients, and quantities in memory. In addition, a parser was implemented for the bizarre format used by the CD-ROM's ingredient lists.
In order for the system to pour drinks correctly, it is necessary for the software to be told which ingredients are connected to which pumps. In order to make this configuration data easy to modify, we created another XML file containing the names of the ingredients connected to each pump along with the quantity remaining of that ingredient. As each drink is poured, the system updates the quantities in this file, so it is possible to avoid running the pumps on an empty bottle and damaging the pumps.
At this point, we were ready to begin GUI development. In the interests of cross-platform support, we decided to use Java Swing as the GUI technology of choice. By this time we also knew the specifications (physical size and pixel resolution) of the LCD touchscreen we would be using in the project. This knowledge allowed us to create a useable touchscreen GUI despite the small size (10.4" diagonal) of the target monitor, and despite our not having possession of the monitor at this point. The guiding principle of the GUI design was to keep things as simple as possible, but still make navigation of a potentially large list of drinks manageable. In addition, because of the touchscreen, we restricted our widget set to simply buttons and labels, and ensured that all buttons were very large so they would make easy targets for users.
An additional requirement we had in mind was a billing system requiring users to pay for their drinks either in advance or at the end of an event. With that in mind, we implemented a login system so users had to be logged-in before using the system. We did not implement any sort of user database in our prototype, though, so any login ID will suffice. There are a series of login IDs that are reserved in order to provide a very rudimentary way of accomplishing certain administrative duties. For example, there are login IDs that will "prime" each pump to ensure that liquid fills the lines after an ingredient has been switched. There is also a login ID that will re-load the configuration XML file from the disk, in order to incorporate any changes (such as switching ingredients) made from another application (such as a text editor).
Here is an example sequence of steps a user would go through to use our system:
First, the user approaches the machine and sees the default menu:
The user will press the "Login" button, and be presented with the login menu:
At the login menu, the user enters any login ID, then presses the "OK" button. They will now be taken to the main menu:
From the main menu, the user can either browse the entire drink list, or to browse only by a certain type of drink. Assume the user opts to pick a drink type first. They will be taken to the list of drink types:
Once the user has picked a drink type, they will be taken to the drink list screen, where the matching drinks will be displayed. If the user presses the button for a particular drink, that drink will be poured immediately.
Note that if the user had gone directly to "browse drink list" from the main menu, they would see this same screen, only with a different list of drinks.
Once their drink is poured, the user would log out and the system returns to its default menu.
Figure 1: Drink Object Model