Saturday, October 25, 2014

Rewriting a Legacy App - Part 2: Working with the Hardware

The first step to rewriting an application is to figure out what we need (this is true when we're working on new applications, too). Last time, we looked at existing features and figured out the bare minimum that we would need to implement to replace existing use.

Note that I said existing *use* and not existing *functionality*. These are two different things. Here are the features that are currently used:

Needed for MVP:
  1. Send commands through the serial dongle
    This is the purpose of the system.
  2. Send commands for 8 devices on House Code "A"
    These are the only devices that are actively used.
  3. Fire on/off/dim events on a schedule
    To maintain the air conditioner functionality (and current lighting schedule).
Step one is to interact with the hardware by sending messages through the serial port. I wrote a test application to make sure that this would work. The code isn't very good, but it gets the job done. We'll clean up the code as we go.

[Update: Code download and links to the entire series of articles: Rewriting a Legacy App]

Communicating with a Serial Port
The first piece of the puzzle is to communicate with a serial port. As a reminder, we have a 9-pin dongle that we hook up to a USB port with an adapter:

Us old-timers (well it wasn't really that long ago) used serial modems to connect to other computers (whether to Wildcat BBSs or other systems). So, we're used to the things that need to be set up for serial port communication -- things like baud rate, parity, number of stop bits, and so forth. If you had the wrong settings for the system you were trying to connect to, then it didn't connect.

When setting up a serial port in .NET, we need the same information. Fortunately, I can bring these settings over from the legacy system -- with one exception. The COM port will vary depending on the hardware.

To figure out the COM port, I just plug in the USB adapter and take a look at the device manager:

This shows that we're using COM3. With this information, we can initialize a serial port.

A Test Library
For the initial test, I created a class library that we can use to send commands to the serial port. I'm expecting that we'll do a lot of refactoring of this code along the way. I called the class "X10Utils", and it has the following initialization code:

I won't go into the details. The only thing that has changed over the years is the baud rate. This started out as a slower value when I first put the legacy project into production. But now, we basically max out what is available with the serial port.

Sending a Message
This is where things get fun. In order to send a message to the system, we enable and disable DTR and RTS. DTR stands for "Data Terminal Ready", and RTS stands for "Request to Send". These are serial port control signals, and they (and others) would determine if a system was listening, sending, waiting, etc.

But these meanings have nothing to do with how they are used here. The use here was decided by whatever engineer designed this interface to the dongle.

So, we enable/disable DTR and RTS in a particular order to send a message. Here's an example of two messages:

The first turns on device A01 (that is device #1 on house code "A"). The second message turns off that same device. I represented the messages as "1"s and "0"s because that's how they were represented in the documentation that I originally used.

For each "1" in the message, DTR needs to be disabled and re-enabled. For each "0" in the message, RTS needs to be disabled and re-enabled. These sequences get translated into an RF signal that makes it's way to the transceiver module (don't ask me how that happens -- I'm not an electrical engineer).

Each message is broken up into 3 parts. The first line (2 bytes) is the header. This is the same for every message. The last line (1 byte) is the footer. This is also the same for every message. The middle line (2 bytes) is the actual message that is being sent.

You've probably noticed that the message bits are represented as strings. I don't think that this is the best way to represent this data, but this is how it was represented in the legacy application. I left it for now since it's easy to copy over the data from the legacy application. Here's a small sample of the data:

You'll notice that the "xA1on" and "xA1off" values correspond to the middle lines of the code sample above. As you might imagine, there's quite a bit of this (16 devices and 16 house codes). This is one reason why I'm limiting things to just the devices being used (#2 from our MVP requirements).

Preparing the Message
There's really 3 parts to sending a message: (1) Prepare for the message, (2) Send the message, (3) Reset when done.

Here's the code to prepare to send the message:

This method takes a parameter to decide whether we're sending an "On" message or an "Off" message. The "message" variable is set to the correct value based on the parameter. (Again, not great code at this point.)

Next, we open the serial port and enable both DTR and RTS. The initializes the hardware device.

Then we have a "Thread.Sleep(10)". Now, Thread.Sleep is usually a bad idea. And one of the first things I want to do is to change this. But to get started, I'm moving the working code over from the legacy system. When I first wrote this code, I found that I needed "Sleep(100)"s in place between processing each bit. And I found that this value would vary depending on the computer. On previous systems, this value has been "100" and "50". I found that on my current computers (which are extremely fast compared to the hardware this was originally running on), I have the value down to "10". We'll work on getting rid of the Sleep a bit later.

Sending the Message
Now that things are prepared, we need to send the actual message. Here's how I process our "message" variable:

I simply "foreach" over each bit of the message. Since these are strings, this comes out as the character "1" or the character "0". Based on that, I click off RTS or DTR.

Again, the "Sleep" values are in there to give things a chance to register.

Resetting Things
After sending the message itself, we reset the serial port:

This closes the serial port once we are done sending the message.

So, there's not all that much code here. Let's see if it works.

Testing the Code
The best way to test this method is to plug things in and run the method. I created a console application to do just that.

This is pretty straightforward. I create an instance of our library class and call the test method twice -- once with "true" to turn the device on and once with "false" to turn the device off.

The main reason I'm doing both commands is because I'm lazy. The Transceiver Module (which is the one we talk to when we talk to the "A01" device), makes a satisfying clicking noise when it changes state -- either clicking on or clicking off. Since I run it through both states, I know that I should expect to hear 1 click (if it's already "on") or 2 clicks (if it's "off"). By sending both commands, I know I'll hear at least 1 click each time.

And when running the application, I do hear the module turning on and off: SUCCESS!

Where Are We?
So this is a really good start. We know that we can interact with the hardware. And this really is a big success. When I was first programming against this hardware, trying to figure out the timings and settings through trial and error, I went through about 2 weeks of frustration. Eventually I found out that I had my "1"s and "0"s inverted. Hearing that first click and seeing a light go on was a huge step forward.

Let's review what we need for our minimum viable product.

Needed for MVP:
  1. Send commands through the serial dongle
    This is the purpose of the system.
  2. Send commands for 8 devices on House Code "A"
    These are the only devices that are actively used.
  3. Fire on/off/dim events on a schedule
    To maintain the air conditioner functionality (and current lighting schedule).
We've accomplished #1: interacting with the hardware. As we move on to #2, we need to do some thinking and refactoring. We'll need to figure out if storing the commands as strings is practical (maybe it is). There may be a better way, but we also may not care about that when we're coming up with a working product. It may be better to spend our time on #3, which is a bit more complicated.

I haven't posted the source code because there really isn't much that you can do with it without the hardware. But if you'd like to see a copy, let me know, and I'll be glad to make it available.

We're well on our way to creating a replacement for an existing application. And it doesn't look like it will take much more work to get the minimum viable product.

Happy Coding!

No comments:

Post a Comment